Skip to content

Creating User friendly Errors

Mark S. Miller edited this page Apr 26, 2023 · 5 revisions

In development, it's often necessary to handle a low-level error and transmute it into a more user-friendly error specific to the context in which the user will understand it.

As a concrete example, in ERTP, a user might try to withdraw an amount from a purse that is greater than the amount that the purse contains. For Nat values, this results in a low-level error from the Nat package such as -65000 is negative where 65000 is the overdrawn amount. However, the user needs to get an error message that isn't low-level, one that tells them that their withdrawal failed. Ideally, the message and error stack would contain enough information for the user to narrow down the action that triggered the error, as well as the purse and payment involved.

The current best practice for turning a low-level error into a user-friendly error is to annotate the user-friendly error with the low-level error, and throw the user-friendly error.

For example, in Zoe we do:

  const handleRejected = lowLevelError => {
    const userFriendlyError = assert.error(
      X`A Zoe invitation is required, not ${invitation}`,
    );
    assert.note(userFriendlyError, X`Due to ${lowLevelError}`);
    throw userFriendlyError;
  };

  return E.when(
    invitationIssuer.burn(invitation),
    handleFulfilled,
    handleRejected,
  );

So a solution to purses that are overdrawn might be:

    let newPurseBalance;
    try {
      newPurseBalance = subtract(currentBalance, amount);
    } catch (err) {
      const withdrawalError = assert.error(
        X`Withdrawal of ${amount} failed because the purse only contained ${currentBalance}`,
      );
      assert.note(withdrawalError, X`Caused by: ${err}`);
      throw withdrawalError;
    }

Or you can preemptively assert that the error condition isn't present:

AmountMath.isGTE(currentBalance, amount) ||
  Fail`Withdrawal of ${amount} failed because the purse only contained ${currentBalance}`;
const newPurseBalance = subtract(currentBalance, amount);

These two techniques have complementary pros and cons

  • The assert.note technique has the pro of avoiding redundant tests. It only does extra work once an error has already been detected. But it has the con of possible mis-reporting. The catch clause assumes it understands why the low level error happened, when reporting this assumed cause in terms of a higher level explanation.
  • The preemptive assertion technique has the con of doing a prior check that is redundant (in the typical success case) with a check in the operation it precedes. It has the pro that the higher level explanation is gated by a narrower accurate check, leaving the low level check as the explanation only for the error cases that the narrow check did not catch.

We can often get the best of both worlds (both pros, neither con) by combining the techniques:

    let newPurseBalance;
    try {
      newPurseBalance = subtract(currentBalance, amount);
    } catch (err) {
      AmountMath.isGTE(currentBalance, amount) ||
        Fail`Withdrawal of ${amount} failed because the purse only contained ${currentBalance}`;
      throw err;
    }

For more information on assert, see the error documentation in Endo.

Clone this wiki locally