Difference between `error` and `fail`

Mod note: As of SDK 1.5.0 Scenarios have been superseded by the more powerful Daml Script. We now recommend using that for all purposes. For more information, and to learn how to use Script please check out @Andreas’ post on our blog.

I just tracked down a bug in my code that I found quite hard (and surprising) to discover. It was related to the difference between error and fail, so I wanted to ask for some clarification on this.

I have the following function:

oneOrNone : Optional a -> Optional a -> Optional a
oneOrNone (Some x) (Some y) = fail "Only one of the optionals may be set"
oneOrNone (Some x) None = Some x
oneOrNone None (Some y) = Some y
oneOrNone None None = None

The following test case passes, which is (at first) unexpected:

test = scenario do
  let
    x = Some "x"
    y = Some "y"
    z = oneOrNone x y
  assert $ z == None

It means my function above returns None in the case with the fail.

Intuitively, I expected fail to just abort the transaction with the corresponding error message. But apparently fail works according to the ActionFail type class implementation, which in the case of Optional seems to just return None, discarding the error message. The docs for fail don’t really explain this behaviour well, just stating: Fail with an error message.

I think my misconception came from the fact that fail indeed aborts the transaction when I’m in the Update or Scenario monad. I just hadn’t realized that that behaviour changes when I’m in the Optional context. It was in fact error that I should’ve used, which seems to do what I wanted here.

So I guess I’ve figured out why I’ve produced this bug. But could anyone shed some light on the two functions, when to use each, and how exactly error works? The documentation of error is not quite clear: it states that “within a transaction” it will abort it. But in my example test case I’m not in any transaction context afaics. So how exactly does this work?

A related question: does the ActionFail instance on Optional (and also [], which I assume produces an empty list) even make sense? What would be valid use cases for fail within the Optional or list monad, wouldn’t I just return None, resp. [] directly instead, as the error message for fail is discarded anyway?

Appreciate any insights on how to better make sense of these functions.

2 Likes

fail is an alias of abort. The difference between error and abort is covered in this section of the Introduction to DAML: https://docs.daml.com/daml/intro/5_Restrictions.html
I think it should answer most of your questions, except on the ActionFail instances. And while I agree that it might be a bit surprising at first, these are the standard implementations and taken over from Haskell. An example which may be a bit more intuitive is the instance for Either Text, where abort t = Left t.

1 Like

ActionFail exists primarily of do-notation. In a do-block you can write something like:

do Some x <- _

You have to handle the pattern match failure somehow. There are two options:

  1. You always crash like for a pattern-match failure in a case statement.
  2. You make the pattern match failure depend on the type of Action and therefore make it configurable.

Haskell chose option 2 (and DAML inherits this from Haskell) which led to the behavior for Optional and [] you know. This can often be convenient for filtering in things like lists, e.g., a very simple implementation of mapOptional can look as follows:

mapOptional f xs = do
  Some x <- map f xs
  pure x
3 Likes

Consider this do block, where foo, bar, baz, and quux stand for arbitrary steps in the do:

do
  foo
  bar
  fail "hi there"
  baz
  quux

Now consider this do evaluated in either Optional or [], where fail will return None or [] respectively. Because of this value, baz and quux will never be reached. That is more or less all we ask of a correct ActionFail instance, and it naturally holds in this example for Update and Scenario as well.

Aside from most occurrences happening implicitly as a result of a pattern, as @cocreature describes, you might want to use it to be generic over these choices.

I would say, as a matter of practice, to use fail instead of error when possible, because fail is less “magical” than error, i.e. a function that can abort by fail must reflect that in its type, whereas error works anywhere, invisibly.

3 Likes