Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save danawoodman/7b2268c12fb69ad85b664fc84ce866b4 to your computer and use it in GitHub Desktop.
Save danawoodman/7b2268c12fb69ad85b664fc84ce866b4 to your computer and use it in GitHub Desktop.
Rust-like "results" for TypeScript projects

Rust-like "results" for TypeScript projects

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.

Implementation

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);
	}
}

Usage

Try<Value, Error>()

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 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.

Ok / Err / Result

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);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment