Result<T, E>: Success<T> | Failure<E>

A Result type represents either Success or Failure.

TL;DR

Result is a minimalist implementation of a value that can be a "success" or a "failure". It borrows from what done in other modern languages (i.e. Rust, Kotlin, Swift, etc.).

The Lens SDK adopts this pattern in order to:

  • be explicit about the known failure scenarios of a task,
  • provide a way for consumers to perform exhaustive error handling,
  • makes control flow easier to reason about.

Type Parameters

Remarks

You might be familiar with the Either type from functional programming. The Result type could be seen as a more specific version of Either where the left side is reserved for success scenarios and the right side is reserved for known failure scenarios.

Think of failure scenarios as alternative outcomes of a given task that although not the "happy path", are still legitimate results for the task within the boundary of a correct usage of the SDK.

In promoting exhaustive error handling, the Lens SDK makes it easier to evolve your code when a new error case is added or a case is removed. For example after a Lens SDK upgrade you can simply run the TS compiler to figure out where you need to handle the new error cases, or even better, it guides you to remove obsolescent code where an error case is no longer possible. This is virtually impossible with a try/catch approach.

Thrown exceptions are historically difficult to trace. They require implicit knowledge of the implementation details of the code that might throw exceptions. This might go several layers down and leads to tight coupling between modules.

The Lens SDK still throws exceptions where the error is not a "normal execution scenario". These are considered real "exceptional circumstances" and not alternative outcomes and it's up to the consumer to try/catch them.

An example of errors that are thrown by the SDK is InvariantError. They are often thrown as result of a misuse of the SDK. By throwing them we want to fail fast so the consumer can fix the issue as soon as possible. Specifically for InvariantError, there is no need to code defensively against these errors. Just rectify the coding issue and move on.

Example

Control flow

const result: Result<number, RangeError> = doSomething();

if (result.isFailure()) {
// because of the `isFailure` check above, TS knows that `result` is a `Failure<RangeError>` here
console.log(result.error); // result.error gets narrowed to `RangeError`

return; // early return
}

// because of the `isFailure` check above and the early return, TS knows that `result` is a `Success<number>` here
console.log(result.value); // result.value gets narrowed to `number`

Example

Exhaustive error handling

Given a result type like the following:

const result: Result<number, PendingSigningError | WalletConnectionError> = doSomething();

You can use a function with a switch statement to perform exhaustive error handling:

function format(failure: Failure<PendingSigningError | WalletConnectionError>): string {
switch (failure.error.name) {
case 'PendingSigningError':
return 'Please sign the transaction';
break;
case 'WalletConnectionError':
return 'Please connect your wallet and try again';
break;
}
// any code after the switch statement is unreachable
}

The example above assumes allowUnreachableCode: false in your tsconfig.json.

An even more robust way to perform exhaustive error handling with a switch is to use the never type: see exhaustiveness checking.

See