Skip to content

@backpack/error-handling 🚩 ​

A package with various utilities to simplify error handling.

Installing ​

First, ensure you have set up Nexus as private registry needed to use Backpack.

Then, install the package using npm:

bash
npm install @backpack/error-handling

Result<T, E> ​

A small utility type representing either a successful value or a failure. Inspired by many other programming languages that offer similar monads.

typescript
type Result<T, E> = Success<T> | Failure<E>;

This object contains:

  • either a value: T with success: true and failed: false
  • or an error: E with success: false and failed: true

Example usage, using the runCatching() function:

typescript
import { runCatching } from "@backpack/error-handling/result";

const result = runCatching(() => mightFail(), MyCustomError);

if (result.failed) {
  console.warn("Got custom error", result.error);
  return;
}

console.log("Got value", result.value);

runCatching<T>(block: () => T): Result<T, unknown> ​

Runs the provided callback, catching any exceptions, and wrapping its result in a Result object.

Optionally accepts one or more error classes, catching only exceptions that are instances of these classes:

typescript
const result = runCatching(
  () => mightFail(),
  MyCustomError,
  MyOtherError,
);

if (result.failed) {
  const error = result.error;
  //    ^ inferred as `MyCustomError | MyOtherError`
}

Additionally, there is also an async version of this utility called runCatchingAsync(), returning a Promise<Result>.

success<T>(value: T): Success<T> ​

Creates a new result, representing a successful value.

failure(error: E): Failure<E> ​

Creates a new result, representing a failure with its associated error.

AsyncResult<T> ​

An extension of Promise<T>, adding more operators to it, to state your intentions more explicitly.

Example:

typescript
import { AsyncResult } from "@backpack/error-handling/async-result";

const value = await AsyncResult.try(() => mightFail())
  .map((r) => r * 2)
  .onFailure((e) => console.warn("Got error", e))
  .recoverIfInstanceOf(MyCustomError, () => "fallback-1")
  .orElse("fallback-2");

console.log("Got value", value);

AsyncResult.try<T>(block: () => T): AsyncResult<T> ​

Executes a callback, wrapping its result in an AsyncResult<T>.

Similar to Promise.try(), can be used to catch all errors in any regular function in a Promise-like style.

.map((v: T) => R) ​

Alias for .then(), added to improve semantics.

.onSuccess((v: T) => void) ​

Executes the given callback if successful, letting the original value pass through.

.onFailure((e: unknown) => void) ​

Executes the given callback if failed, letting the original error pass through.

.onFailureIfInstanceOf(errorClass, (e: E) => void) ​

Executes the given callback if failed, if the error is an instance of the class provided, letting the original error pass through.

.recover(recoverFn) ​

If failed, maps the error to another value, to recover into a successful AsyncResult.

.recoverIf(predicate, recoverFn) ​

If failed, maps the error to another value if it matches the given predicate, to recover into a successful AsyncResult.

.recoverIfInstanceOf(errorClass, recoverFn) ​

If failed, maps the error to another value if it is an instance of the provided error class, to recover into a successful AsyncResult.

.match(onSuccess, onFailure) ​

Maps the value if successful, or else the error if failed.

Same as .map(onSuccess).orElseGet(onFailure).

.orElse(value) ​

Alias for .recover(() => value) to improve semantics, e.g. to indicate a default error handler at the end of a chain.

.orElseGet(() => value) ​

Alias for .recover(() => value) to improve semantics, e.g. to indicate a default error handler at the end of a chain.

.asResult() ​

Converts the AsyncResult<T> into a Promise<Result<T, unknown>>. Can be used in combination with async/await to handle the Result in a more imperative style.

Promise utilities ​

Most methods on AsyncResult also exist as higher-order functions, so they can be applied on regular Promise chains.

For example:

typescript
const value = await promiseTry(() => mightFail())
  .then(map((r) => r * 2))
  .catch(onFailure((e) => console.warn("Got error", e)))
  .catch(recoverIfInstanceOf(MyCustomError, () => "fallback-1"))
  .catch(orElse("fallback-2"));
  • promiseTry(): creates a new Promise<T> from any sync or async function
  • higher-order functions to be used with .then():
    • map(): syntactic sugar to express a map operation
    • onSuccess(): to introduce a side effect upon success
  • higher-order functions to be used with .catch():
    • onFailure(): to introduce a side effect upon failure
    • onFailureIfInstanceOf(): to introduce a side effect only for certain failures
    • recover(): to recover from any failure
    • recoverIf(): to recover only from specific failures
    • recoverIfInstanceOf(): to recover from certain types of failures
    • orElse(): same as recover, to express a final fallback value;
    • orElseGet(): same as orElse, but allows to provide an arrow function.

catchAll() ​

An additional .catch() operator that applies all given callbacks onto the resulting Promise, returning the aggregated result.

@catchErrors() decorator ​

Backpack also offers a decorator which can be applied on methods that return a Promise<T>. This decorator accepts one or more .catch() callbacks, which will be applied in the provided order.

Example:

typescript
import { catchErrors } from "@backpack/error-handling/promises";

class MyService {
  @catchErrors((e) => "fallback")
  public async mightFail(): Promise<string> {
    // ...

    return "foo";
  }
}

All the higher-order functions mentioned above, which are supposed to be used with .catch(), can also be used in combination with the @catchErrors() decorator:

typescript
class MyService {
  @catchErrors(
    onFailure((e) => console.error("Got error", e)),
    recoverIfInstanceOf(MyError, () => "fallback 1"),
    orElse("fallback-2"),
  )
  public async mightFail(): Promise<string> {
    // ...

    return "foo";
  }
}

This also allows us to use Lambda error handlers, such as provided by @backpack/aws-lambda, or by easily creating one yourself:

typescript
export class MyLambda {
  @catchErrors(myErrorHandler())
  public async handle(): Promise<APIGatewayProxyResult> {
    // ...
  }
}

const myErrorHandler = () =>
  catchAll(
    recoverIfInstanceOf(MyCustomError, myCustomErrorConverter()),
    recoverIfInstanceOf(ProblemError, problemErrorConverter()),
    orElseGet((error) =>
      problemResult({
        type: "/problems/internal-server-error",
        title: "Internal Server Error",
        detail: "Something went wrong.",
        status: 500,
      }),
    ),
  );

About decorators

Native ECMAScript decorators are currently in stage 3 of the TC39 standardization process, and are not yet implemented in Node.js.

However, this is not a problem if your code is transpiled. By using Amazon Lambda Node.js CDK construct which uses esbuild under the hood, this is already taken care of.