Handling errors in JavaScript/TypeScript kinda sucks. throw
ing 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>>;
export function Ok<T>(value: T): Ok<T> {
const result: Ok<T> = { ok: true, value };
return result;
}
export function Err<E>(value: E): Err<E> {
const result: Err<E> = { ok: false, value };
return result;
}
export function Try<T, E = Error>(
fn: () => T | Promise<T>,
): Result<T, E> | Promise<Result<T, E>> {
try {
const result = fn();
if (result instanceof Promise) {
return result.then(Ok).catch((error) => Err(error as E));
}
return Ok(result);
} catch (error) {
return Err(error as E);
}
}
Try is useful if you want automatically catching of throw
n 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 Try<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 or 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);
}