Skip to main content
The @temelj/result package provides a Result monad for functional error handling in TypeScript. It allows you to handle errors explicitly without using try-catch blocks, making error handling more predictable and type-safe.

Installation

npm install @temelj/result

Result type

A Result<T, E> represents either a success (Ok) or a failure (Err).
type Result<T, E> = ResultOk<T> | ResultErr<E>

interface ResultOk<T> {
  readonly kind: "ok";
  readonly value: T;
}

interface ResultErr<E> {
  readonly kind: "error";
  readonly error: E;
}

Creating results

ok

Creates a success result.
function ok<T>(value: T): ResultOk<T>
import { ok } from "@temelj/result";

const result = ok(42);
// Result: { kind: "ok", value: 42 }

err

Creates an error result.
function err<E>(error: E): ResultErr<E>
import { err } from "@temelj/result";

const result = err("Something went wrong");
// Result: { kind: "error", error: "Something went wrong" }

Type guards

isOk

Checks if a result is a success.
function isOk<T, E>(result: Result<T, E>): result is ResultOk<T>
import { isOk, ok, err } from "@temelj/result";

const result = ok(42);

if (isOk(result)) {
  console.log(result.value); // TypeScript knows this is ResultOk<number>
}

isErr

Checks if a result is an error.
function isErr<T, E>(result: Result<T, E>): result is ResultErr<E>
import { isErr, ok, err } from "@temelj/result";

const result = err("Failed");

if (isErr(result)) {
  console.error(result.error); // TypeScript knows this is ResultErr<string>
}

Unwrapping results

unwrap

Extracts the value from a success result, or throws if it’s an error.
function unwrap<T, E>(result: Result<T, E>): T
unwrap throws the error if the result is an error. Use only when you’re certain the result is Ok.
import { unwrap, ok, err } from "@temelj/result";

const result = ok(42);
console.log(unwrap(result)); // 42

const errorResult = err("Failed");
unwrap(errorResult); // Throws "Failed"

unwrapErr

Extracts the error from an error result, or throws if it’s a success.
function unwrapErr<T, E>(result: Result<T, E>): E
import { unwrapErr, err } from "@temelj/result";

const result = err("Something went wrong");
console.log(unwrapErr(result)); // "Something went wrong"

unwrapOr

Extracts the value from a success result, or returns a default value if it’s an error.
function unwrapOr<T, E>(
  result: Result<T, E>,
  defaultValue: T | (() => T),
): T
import { unwrapOr, err } from "@temelj/result";

const result = err("Failed");
const value = unwrapOr(result, 0);
// value: 0

Transforming results

map

Transforms the value inside a success result.
function map<T, E, U>(
  result: Result<T, E>,
  fn: (value: T) => U,
): Result<U, E>
import { map, ok, err } from "@temelj/result";

const result = ok(42);
const doubled = map(result, (n) => n * 2);
// Result: ok(84)

const errorResult = err("Failed");
const stillError = map(errorResult, (n) => n * 2);
// Result: err("Failed") - error unchanged

mapErr

Transforms the error inside an error result.
function mapErr<E, F>(
  result: Result<any, E>,
  fn: (error: E) => F,
): Result<any, F>
import { mapErr, err, ok } from "@temelj/result";

const result = err("network error");
const mapped = mapErr(result, (e) => e.toUpperCase());
// Result: err("NETWORK ERROR")

const okResult = ok(42);
const stillOk = mapErr(okResult, (e) => e.toUpperCase());
// Result: ok(42) - value unchanged

Converting from throwables

fromThrowable

Calls a synchronous function that may throw and returns a Result.
function fromThrowable<T>(fn: () => T): Result<T, unknown>
function fromThrowable<T, E>(
  fn: () => T,
  onErr: (e: unknown) => E,
): Result<T, E>
import { fromThrowable } from "@temelj/result";

const result = fromThrowable(() => {
  return JSON.parse('{"valid": "json"}');
});
// Result: ok({ valid: "json" })

const errorResult = fromThrowable(() => {
  return JSON.parse('invalid json');
});
// Result: err(SyntaxError)

fromPromise

Calls an async function and returns a Promise<Result>. Catches both synchronous exceptions and asynchronous rejections.
function fromPromise<T>(
  fn: () => Promise<T>,
): Promise<Result<T, unknown>>

function fromPromise<T, E>(
  fn: () => Promise<T>,
  onErr: (e: unknown) => E,
): Promise<Result<T, E>>
import { fromPromise, isOk } from "@temelj/result";

const result = await fromPromise(async () => {
  const response = await fetch("/api/data");
  return await response.json();
});

if (isOk(result)) {
  console.log("Data:", result.value);
} else {
  console.error("Failed:", result.error);
}

Practical examples

Parsing user input

import { fromThrowable, map, unwrapOr } from "@temelj/result";

function parseAge(input: string): number {
  const result = fromThrowable(
    () => {
      const num = parseInt(input, 10);
      if (isNaN(num) || num < 0 || num > 150) {
        throw new Error("Invalid age");
      }
      return num;
    },
    (e) => (e as Error).message
  );

  return unwrapOr(result, 0);
}

console.log(parseAge("25")); // 25
console.log(parseAge("invalid")); // 0
console.log(parseAge("200")); // 0

Chaining operations

import { fromPromise, map, isOk } from "@temelj/result";

async function getUserProfile(userId: string) {
  const userResult = await fromPromise(
    async () => await fetchUser(userId),
    (e) => `Failed to fetch user: ${e}`
  );

  if (!isOk(userResult)) {
    return userResult;
  }

  const profileResult = map(userResult, (user) => ({
    id: user.id,
    name: user.name,
    displayName: `${user.firstName} ${user.lastName}`,
  }));

  return profileResult;
}

Error handling without try-catch

import { fromPromise, isErr } from "@temelj/result";

async function processData() {
  const result = await fromPromise(
    async () => await fetchAndProcessData()
  );

  if (isErr(result)) {
    logger.error("Processing failed", result.error);
    return null;
  }

  return result.value;
}

Benefits of Result type

The Result type forces you to handle errors explicitly at compile time. TypeScript will ensure you check whether a result is Ok or Err before accessing the value.
Unlike exceptions, Results don’t introduce hidden control flow. You can see exactly where errors might occur and how they’re handled.
Results can be easily composed using map, mapErr, and other utilities, making it simple to build complex error handling logic.
While you could use union types like T | Error, Result provides a structured way to differentiate success and failure with type guards and utilities.

Pattern matching

You can use JavaScript’s pattern matching with Results:
import { type Result, ok, err } from "@temelj/result";

function handle(result: Result<number, string>) {
  switch (result.kind) {
    case "ok":
      return `Success: ${result.value}`;
    case "error":
      return `Error: ${result.error}`;
  }
}

console.log(handle(ok(42))); // "Success: 42"
console.log(handle(err("failed"))); // "Error: failed"
The Result type is inspired by Rust’s Result enum and provides similar ergonomics for error handling in TypeScript.

Build docs developers (and LLMs) love