8 Exception Handling

The default behavior in Daml is to abort the transaction on any error and roll back all changes that have happened until then. However, this is not always appropriate. In some cases, it makes sense to recover from an error and continue the transaction instead of aborting it.

One option for doing that is to represent errors explicitly via Either or Option as shown in 3 Data types. This approach has the advantage that it is very explicit about which operations can fail and which cannot. However, it also has some downsides. First, it can be invasive for operations where aborting the transacton is often the desired behavior, e.g., changing division to return Either or an Option to handle division by zero would be a very invasive change and many callsites might not want to handle the error case explicitly. Second, and more importantly, this approach does not allow rolling back ledger actions that have happened up to this point: If a contract got created before we hit the error, there is no way to undo this.

Exceptions provide a way to handle certain types of errors in a non-invasive way and to roll back parts of the transaction that lead up to this error. All of that still happens within the same transaction and is thereby atomic contrary to handling the error outside of Daml.

Hint

Remember that you can load all the code for this section into a folder called 8_Exceptions by running daml new 8Exceptions --template daml-intro-8

Our example for the use of exceptions will be a simple shop template. Users can order items by calling a choice and transfer money (in the form of an Iou issued by their bank) from their account to the owner in return.

First, we need to setup a template to represent the account of a user.

template Account with
    issuer : Party
    owner : Party
    amount : Decimal
  where
    signatory issuer, owner
    ensure amount > 0.0
    key (issuer, owner) : (Party, Party)
    maintainer key._2

    choice Transfer : () with
        newOwner : Party
        transferredAmount : Decimal
      controller owner, newOwner
      do create this with amount = amount - transferredAmount
         create Iou with issuer = issuer, owner = newOwner, amount = transferredAmount
         pure ()

Note that the template has an ensure clause that ensures that the amount is always positive so Transfer cannot transfer more money than is available.

The shop is represented as a template signed by the owner. It has a field to represent the bank accepted by the owner as well as a list of observers that can order items.

template Shop
  with
    owner : Party
    bank : Party
    observers : [Party]
  where
    signatory owner
    observer observers
    let price: Decimal = 100.0

The ordering process is then represented by a non-consuming choice on this template which calls Transfer and creates an Order contract in return.

    nonconsuming choice OrderItem : ContractId Order
      with
        shopper : Party
      controller shopper
      do exerciseByKey @Account (bank, shopper) (Transfer owner price)
         create Order
           with
             shopOwner = owner
             shopper = shopper

However, the shop owner has realized that often orders fail because the account of their users is not topped up. They have a small trusted userbase they know well so they decide that if the account is not topped up, the shoppers can instead issue an Iou to the owner and pay later. While it would be possible to check the conditions under which Transfer will fail in OrderItem this can be quite fragile: In this example, the condition is relatively simple but in larger projects replicating the conditions outside the choice and keeping the two in sync can be challenging.

Exceptions allow us to handle this differently. Rather than replicating the checks in Transfer, we can instead catch the exception thrown on failure. To do so we need to use a try-cach block. The try block defines the scope within which we want to catch exceptions while the catch clauses define which exceptions we want to catch and how we want to handle them. In this case, we want to catch the exception thrown by a failed ensure clause. This exception is defined in daml-stdlib as PreconditionFailed. Putting it together our order process for trusted users looks as follows:

    nonconsuming choice OrderItemTrusted : ContractId Order
      with
        shopper : Party
      controller shopper
      do try do
           exerciseByKey @Account (bank, shopper) (Transfer owner price)
           pure ()
         catch
           PreconditionFailed _ -> do
             create Iou with
               issuer = shopper
               owner = owner
               amount = price
             pure ()
         create Order
           with
             shopOwner = owner
             shopper = shopper

In addition to catching builtin exceptions like PreconditionFailed, you can also define your own exception types which can be caught and thrown. As an example, let’s consider a variant of the Transfer choice that only allows for transfers up to a given limit. If the amount is higher than the limit, we throw an exception called TransferLimitExceeded.

We first have to define the exception and define a way to represent it as a string. In this case, our exception should store the amount that someone tried to transfer as well as the limit.

exception TransferLimitExceeded
  with
    limit : Decimal
    attempted : Decimal
  where
    message "Transfer of " <> show attempted <> " exceeds limit of " <> show limit

To throw our own exception, you can use throw in Update and Script or throwPure in other contexts.

    choice TransferLimited : () with
        newOwner : Party
        transferredAmount : Decimal
      controller owner, newOwner
      do let limit = 50.0
         when (transferredAmount > limit) $
           throw TransferLimitExceeded with
             limit = limit
             attempted = transferredAmount
         create this with amount = amount - transferredAmount
         create Iou with issuer = issuer, owner = newOwner, amount = transferredAmount
         pure ()

Finally, we can adapt our choice to catch this exception as well:

    nonconsuming choice OrderItemTrustedLimited : ContractId Order
      with
        shopper : Party
      controller shopper
      do try do
           exerciseByKey @Account (bank, shopper) (Transfer owner price)
           pure ()
         catch
           PreconditionFailed _ -> do
             create Iou with
               issuer = shopper
               owner = owner
               amount = price
             pure ()
           TransferLimitExceeded _ _ -> do
             create Iou with
               issuer = shopper
               owner = owner
               amount = price
             pure ()
         create Order
           with
             shopOwner = owner
             shopper = shopper

For more information on exceptions, take a look at the language reference.

Next up

We have now seen how to develop safe models and how we can handle errors in those modules in a robust and simple way. But the journey doesn’t stop there. In 9 Working with Dependencies you will learn how to extend an already running application to enhance it with new features. In that context you’ll learn a bit more about the architecture of Daml, about dependencies, and identifiers.