Error handling
Error handling in JavaScript can be challenging. On this page, you’ll find a few best practices to help simplify error management.
Tip: looking for the code?
This page focuses on best practices. If you're looking for the Backpack error handling utilities, please visit the @backpack/error-handling
API reference.
Why error handling?
There are a few reasons to add control flow to handle errors:
- Consuming an error: you want to act on the error, such as logging or other side effects.
- Recovering from an error: you want to recover by converting and implement a fallback.
Common issues with try/catch
Using JavaScript’s built-in try/catch
flow often introduces some common issues. Take this function as an example:
function mightFail() {
if (Math.random() < 0.5) {
throw new MyCustomError("Epic fail!");
}
return 42;
}
Calling this function without error handling is straightforward:
const value = mightFail();
However, when you want to recover from the MyCustomError
(and only that one), a typical try/catch
will start to look like this:
let value: number;
try {
value = mightFail();
} catch (error) {
if (error instanceof MyCustomError) {
console.warn('Got custom error', error);
x = 10
} else {
throw error;
}
}
console.log('Got value', x)
This leads to verbose, hard-to-read code with several issues:
- Scoping issues: JavaScript doesn’t allow
try/catch
expressions, so thevalue
declaration must be pulled outside thetry
block. This requires you to changeconst
to the non-finallet
. - Type assertions: Since JavaScript catches all errors, you need to explicitly check the type of the caught error.
- Awkward rethrowing: To handle only specific errors, you need to rethrow those outside the desired scope.
Embracing the Result
monad
Many programming languages use the Result
monad type to manage errors elegantly. This is a wrapper that either holds a successful value or an error.
In TypeScript, a synchronous Result
monad can be expressed as a union type:
type Result<TValue, TError> = Success<TValue> | Failure<TError>;
export interface Success<T> {
value: T;
}
export interface Failure<E> {
error: E;
}
There are many implementations of this pattern, but @backpack/error-handling
happily provides one for you.
To create a Result
, you can use the runCatching()
function:
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);
Check out @backpack/error-handling
to learn more!
Leveraging promises
JavaScript includes a built-in monad for error handling: the Promise<T>
type.
While promises are often associated with asynchronous code, you can also use them to capture synchronous code that might fail. Furthermore, if your code becomes asynchronous later, no refactoring is required because promises handle both plain values and PromiseLike
values natively.
Common pitfall
When handling errors with Promise
, it might seem convenient to rely on await
inside a try/catch
block. However, this reintroduces the issues we discussed earlier.
Instead, consider using the .catch()
operator directly with await
:
const value = await myPromiseValue.catch(() => "fallback value");
In @backpack/error-handling
, you’ll also find higher-order functions to simplify working with promises. These functions make common patterns easier to express and your code's intent more visible:
import {
promiseTry,
map,
onFailure,
recoverIfInstanceOf,
orElse,
} from "@backpack/error-handling/promises";
const value = await promiseTry(() => mightFail())
.then(map((r) => r * 2))
.catch(onFailure(() => console.log("...")))
.catch(recoverIfInstanceOf(MyCustomError, () => "fallback-1"))
.catch(orElse("fallback-2"));
console.log("Got value", value);
Alternatively, you can use the AsyncResult<T>
monad, an extension of Promise<T>
. This class provides these higher-order methods directly on the object:
import { AsyncResult } from "@backpack/error-handling/async-result";
const value = await AsyncResult.try(() => mightFail())
.map((r) => r * 2)
.onFailure(() => console.log("..."))
.recoverIfInstanceOf(MyCustomError, () => "fallback-1")
.orElse("fallback-2");
console.log("Got value", value);
Check out @backpack/error-handling
for more details!
Error handling in REST Lambdas
When handling errors in AWS Lambda functions that implement REST endpoints, consider the following best practices:
- Return semantically correct HTTP status codes (required).
- Avoid exposing sensitive information in error responses (required).
- When providing error details, consider adopting the RFC-9457 Problem standard (recommended).
To facilitate standardization, @backpack/aws-lambda
includes built-in error types and handlers.