How to use try catch in DAML Triggers?

@cocreature @akrmn I am trying to catch exceptions in DAML Triggers that occurred by exercising some choice. And I want to create another contract when I catch the exception.

when (isSome bondToken) do
              let bondTokenContractId = fst $ fromSome bondToken
              now <- getTime
              let isSender = (\part -> part.system == m.system)
              let replyTo = List.head $ filter (\(_, part) -> isSender part) parts
              try do    
                T.dedupExercise bondTokenContractId (BondToken_Model.Tranfer_Bond_Tokens cakEscrowAccount numberOfTokens)
                debug $ "Bond token transferred for " <> show settlementInstructionData.transactionId
                whenSome replyTo $ \(system, _) ->
                  T.dedupExercise system (Load.Create_Cash_Transfer_Request)
              catch
                (InsufficientBalanceException _ _) -> do
                  debug $ "error occurred"
                  T.dedupCreate ABCTemplate with a=a, b=b

But it is giving me below error

• No instance for (ActionCatch (TriggerA ()))
        arising from a use of ‘_tryCatch’
    • In a stmt of a 'do' block:
        _tryCatch
          \ _
            -> do dedupExercise
                    bondTokenContractId
                    (Tranfer_Bond_Tokens cakEscrowAccount numberOfTokens)
                  debug
                    $ "Bond token transferred for "
                        <> show (getField @"transactionId" settlementInstructionData)
                  ....
          \case
            fromAnyException -> Some (InsufficientBalanceException _ _)
              -> Some
                   do debug $ "error occurred"
                      dedupCreate
                        CashReversalRequest
                          {endToEndId = (getField @"endToEndId" m),
                           customerRef = (getField @"customerRef" m),
                           payer = (getField @"payer" m),
                           payerAccount = (getField @"payerAccount" m),
                           payee = (getField @"payee" m),
                           payeeAccount = (getField @"payeeAccount" m),
                           instructedMvUnit = (getField @"instructedMvUnit" m),
                           instructedMvType = (getField @"instructedMvType" m),
                           remittanceInfo = (getField @"remittanceInfo" m), createTime = now,
                           settlementBank = (getField @"settlementBank" m), observers = ...,
                           valueDate = (getField @"valueDate" m),
                           chargeBearer = (getField @"chargeBearer" m),
                           purposeCode = (getField @"purposeCode" m), reversalType = LOAD,
                           requestor = (getField @"system" m)}
            _ -> None
      In the second argument of ‘when’, namely
        ‘do let bondTokenContractId = fst $ fromSome bondToken
            now <- getTime
            let isSender = (\ part -> ...)
            let replyTo = List.head $ filter (\ (_, part) -> ...) parts
            ....’
      In a stmt of a 'do' block:
        when
          (isSome bondToken)
          do let bondTokenContractId = fst $ fromSome bondToken
             now <- getTime
             let isSender = (\ part -> ...)
             let replyTo = List.head $ filter (\ (_, part) -> ...) parts
             ....typecheck

How to resolve this?

Anyone who has used try catch and came across same issue, request you to share the resolution for the same

Hi @Pris17 , Triggers’ interaction model is the ledger is entirely asynchronous. When you call T.dedupExercise, the Trigger doesn’t wait for the command to return, or indeed even sends that command off at that very moment. Think of it as putting the command in a queue for the Trigger to send off in the background.

You’ll get the result back as a Message to the updateState function you supplied to the Trigger.

Specifically, in case of hitting a user exception like InsufficientBalanceException, you’ll get a Failure with a message containing the reason of failure.

You can then use that information to update your custom trigger state - eg by putting it into an error queue - and react to the failure in the next run of rule.

1 Like

Thanks for the reply.

Do you have any examples for the same?

Here’s the gsg-trigger examples extended to do just this:

module ChatBot where

import qualified Daml.Trigger as T
import qualified User
import qualified DA.List.Total as List
import DA.Action (when)
import DA.Optional (whenSome)
import DA.Foldable (forA_)

-- Type to pass command failures around in custom state
data CommandFailure = CommandFailure with
  commandId : T.CommandId
  status : Int
  message : Text
    deriving (Eq, Show)

-- Template to record command failures
template ErrorRecord
  with
    p : Party
    f : CommandFailure
  where
    signatory p

-- Keep a list of CommandFailures as custom state
autoReply : T.Trigger [CommandFailure]
autoReply = T.Trigger
  { initialize = pure []
  -- record all failures in custom state
  , updateState = \m -> case m of
      T.MCompletion T.Completion{commandId, status=T.Failed{status, message}} -> do
        T.modify (\failures -> (CommandFailure with commandId; status; message) :: failures)
      _ -> return ()
  , rule = \p -> do
      -- handle command failures by writing them into an ErrorRecord
      failures <- T.get
      T.put []
      forA_ failures (\f -> T.dedupCreate ErrorRecord with p; f)

      message_contracts <- T.query @User.Message
      let messages = map snd message_contracts
      debug $ "Messages so far: " <> show (length messages)
      let lastMessage = List.maximumOn (.receivedAt) messages
      debug $ "Last message: " <> show lastMessage
      whenSome lastMessage $ \m ->
        when (m.receiver == p) $ do
          users <- T.query @User.User
          debug users
          let isSender = (\user -> user.username == m.sender)
          let replyTo = List.head $ filter (\(_, user) -> isSender user) users
          whenSome replyTo $ \(sender, _) ->
            T.dedupExercise sender (User.SendMessage p "Please, tell me more about that.")
  , registeredTemplates = T.AllInDar
  , heartbeat = None
  }

@bernhard @cocreature In my case, exceptions occur while exercising a choice. And in trigger, I need to handle this exception. If there is exception then I need to create another contract in trigger.

How to handle this scenario?

Thanks

For an exception thrown from your Daml code you will get a failed completion. Bernhard’s example shows you how to react to that.

Another option worth considering is whether you want to change your Daml code to not throw an exception, e.g., return an Optional and return None instead of throwing an error. That way, failures may become easier to handle.

Any examples for 2nd option?

It really depends on your app but one example is to use a lookupByKey instead of a fetchByKey so your choice doesn’t fail in the case where there is no contract. You could also do more than just return an Optional, e.g., create another contract in the failure case:

nonconsuming choice CFail : ContractId T
      controller p
      do fst <$> fetchByKey @T k

nonconsuming choice COptional : Optional (ContractId T)
      controller p
      do lookupByKey @T k

Or you use Daml’s Exception Handling feature to catch the error atomically and write it to the ledger.

template ErrorHandler
  with
     p : Party
  where
    signatory p
    key p : Party
    maintainer key

    nonconsuming choice Safe_Transfer_Bond_Tokens : Boolean
      with
        cid : ContractId BondToken
        choiceArgs : BondToken_Model.Tranfer_Bond_Tokens 
     controller p
     do
      try do    
        exercise cid choiceArgs
      catch
        (InsufficientBalanceException _ _) -> do
          T.dedupCreate ABCTemplate with a=a, b=b

Then in your trigger you don’t exercise the choice directly, but do an exerciseByKey on the safe variant.