Handling errors in JavaScript/TypeScript kinda sucks. throwing breaks the control flow, is not "discoverable" (e.g. impossible to really know what will happen in error states), and it is generally more tricky to get well typed error responses.
Taking inspiration from Rust's Result type, we can implement something similar in TypeScript with a minimal amount of code and no dependencies. It's not as robust as Rust's of course, but it has been very useful for me on a variety of large projects.
Save the following TypeScript in your project and import it to use the various result utils:
export interface Ok<T> {
ok: true;
value: T;
}
export interface Err<E> {
ok: false;
value: E;
}
export type Result<T, E = Error> = Ok<T> | Err<E>;
export type AsyncResult<T, E = Error> = Promise<Result<T, E>>;
/**
* Create a success result.
*/
export function Ok<T>(value: T): Ok<T> {
const result: Ok<T> = { ok: true, value };
return result;
}
/**
* Create an error result.
*/
export function Err<E>(value: E): Err<E> {
const result: Err<E> = { ok: false, value };
return result;
}
/**
* A lightweight, synchronous wrapper around a function that returns a value or throws an error.
*/
export function Try<T, E = Error>(
fn: () => T,
fallback?: (error: unknown) => E,
): Result<T, E> {
try {
const result = fn();
return Ok(result);
} catch (error) {
return Err(fallback ? fallback(error) : (error as E));
}
}
/**
* A lightweight, asynchronous wrapper around a function that returns a promise or throws an error.
*/
export async function TryAsync<T, E = Error>(
fn: () => Promise<T> | T,
fallback?: (error: unknown) => E,
): Promise<Result<T, E>> {
try {
const result = await fn();
return Ok(result);
} catch (error) {
return Err(fallback ? fallback(error) : (error as E));
}
}Try is useful if you want automatically catching of thrown errors (auto-wrapped with Err(error)) and simple handling of return values (you just have to return a raw value which will get auto-wrapped with an Ok(value) response.
This is probably what you'll want to use in most cases.
import { Try } from "./results";
import { someAsyncFuncThatCouldThrow } from "./some-async-func-that-could-throw";
// Use Try to wrap code and automatically return a `Result` type
function handleUnexpectedFailures() {
return TryAsync<string>(async () => {
const result = await someAsyncFuncThatCouldThrow();
return `here is the result - ${result}`;
});
}
const result = await handleUnexpectedFailures();
if (!result.ok) {
// Failures will return `false` for `result.ok`:
console.error(
"failed!",
result.value // result.value will be an instance of Error
);
} else {
// if success, result.ok will be true and the value will be the result of the function
console.log("value:", result.value);
}You can use Try() with sync functions or TryAsync() with async functions.
If you want to manually return results you can use the Ok and Err utils. This is useful in cases where you want typed error responses or where you don't need to handle code that might throw:
import { Ok, Err, type Result } from "./results";
// Use Try to wrap code and automatically return a `Result` type.
// Optionally type the response using Result<T, E>
function manuallyReturnResults(): Result<string, string> {
if (Math.random() > 0.5) return Err("Failed");
return Ok("Success");
}
const res1 = manuallyReturnResults();
if (!res1.ok) {
console.error("failed!", res1.value);
} else {
console.log("value:", res1.value);
}