Skip to main content

Overview

The Result type has four primary ways to create instances:
  • Result.ok() for successful values
  • Result.err() for error values
  • Result.try() for wrapping sync functions that may throw
  • Result.tryPromise() for wrapping async functions with retry support

Creating Success Results

Result.ok()

Creates an Ok instance wrapping a successful value.
const success = Result.ok(42);
// Ok<number, never>

const withObject = Result.ok({ id: 1, name: "Alice" });
// Ok<{ id: number, name: string }, never>

Creating Ok<void>

For side-effectful operations that don’t return a meaningful value, call Result.ok() without arguments:
const saveUser = (user: User): Result<void, SaveError> => {
  // ... save logic ...
  return Result.ok(); // Ok<void, SaveError>
};

const result = saveUser(user);
if (result.isOk()) {
  console.log("Saved successfully");
}
Result.ok() creates an Ok<void, never> which is compatible with Result<void, E> for any error type E.

Type Signature

function ok(): Ok<void, never>;
function ok<A, E = never>(value: A): Ok<A, E>;

Creating Error Results

Result.err()

Creates an Err instance wrapping an error value.
const failure = Result.err("Something went wrong");
// Err<never, string>

const withError = Result.err(new Error("Network timeout"));
// Err<never, Error>

Using TaggedError for Discriminated Errors

For type-safe error handling, use TaggedError to create discriminated error classes:
class NotFoundError extends TaggedError("NotFoundError")<{
  id: string;
  message: string;
}>() {}

class ValidationError extends TaggedError("ValidationError")<{
  field: string;
  message: string;
}>() {}

type AppError = NotFoundError | ValidationError;

const fetchUser = (id: string): Result<User, AppError> => {
  if (!id) {
    return Result.err(new ValidationError({ 
      field: "id", 
      message: "ID is required" 
    }));
  }
  // ...
};
See Error Handling for comprehensive TaggedError patterns.

Type Signature

function err<T = never, E = unknown>(error: E): Err<T, E>;

Wrapping Throwing Functions

Result.try()

Executes a synchronous function and wraps the result or exception in a Result.

Basic Usage

const parseJSON = (str: string): Result<unknown, UnhandledException> => {
  return Result.try(() => JSON.parse(str));
};

const result = parseJSON('{"key": "value"}');
// Ok({ key: "value" })

const failed = parseJSON('invalid');
// Err(UnhandledException)

Custom Error Handling

Transform caught exceptions into domain-specific errors using the catch handler:
class ParseError extends TaggedError("ParseError")<{
  input: string;
  message: string;
  cause: unknown;
}>() {}

const parseJSON = (str: string): Result<unknown, ParseError> => {
  return Result.try({
    try: () => JSON.parse(str),
    catch: (cause) => new ParseError({
      input: str,
      message: `Failed to parse JSON: ${str.slice(0, 50)}`,
      cause,
    }),
  });
};
If your catch handler throws, Result.try will throw a Panic. Catch handlers should always return an error value, never throw.

Retry on Failure

let attempts = 0;
const result = Result.try(
  () => {
    attempts++;
    if (attempts < 3) throw new Error("fail");
    return "success";
  },
  { retry: { times: 3 } }
);
// Ok("success") after 3 attempts

Type Signature

function try<A>(
  thunk: () => A,
  config?: { retry?: { times: number } }
): Result<A, UnhandledException>;

function try<A, E>(
  options: { 
    try: () => A; 
    catch: (cause: unknown) => E 
  },
  config?: { retry?: { times: number } }
): Result<A, E>;

Wrapping Async Functions

Result.tryPromise()

Executes an async function and wraps the result or rejection in a Result, with advanced retry support.

Basic Usage

const fetchUser = async (id: string) => {
  const result = await Result.tryPromise(() => 
    fetch(`/api/users/${id}`).then(r => r.json())
  );
  
  return result; // Result<User, UnhandledException>
};

Custom Error Handling

class ApiError extends TaggedError("ApiError")<{
  status: number;
  message: string;
  cause: unknown;
}>() {}

const fetchUser = async (id: string): Promise<Result<User, ApiError>> => {
  return Result.tryPromise({
    try: async () => {
      const response = await fetch(`/api/users/${id}`);
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }
      return response.json();
    },
    catch: (cause) => new ApiError({
      status: cause instanceof Error ? 500 : 0,
      message: cause instanceof Error ? cause.message : String(cause),
      cause,
    }),
  });
};

Retry with Exponential Backoff

const fetchWithRetry = async (url: string) => {
  return Result.tryPromise(
    () => fetch(url).then(r => r.json()),
    {
      retry: {
        times: 3,
        delayMs: 100,
        backoff: "exponential", // 100ms, 200ms, 400ms
      },
    }
  );
};

Conditional Retry

Use shouldRetry to retry only specific errors:
class RetryableError extends TaggedError("RetryableError")<{
  message: string;
  cause: unknown;
}>() {}

class FatalError extends TaggedError("FatalError")<{
  message: string;
  cause: unknown;
}>() {}

type ApiError = RetryableError | FatalError;

const result = await Result.tryPromise(
  {
    try: () => callApi(url),
    catch: (e) => {
      if (e instanceof TypeError) {
        return new RetryableError({ message: "Network error", cause: e });
      }
      return new FatalError({ message: "Fatal error", cause: e });
    },
  },
  {
    retry: {
      times: 3,
      delayMs: 100,
      backoff: "exponential",
      shouldRetry: (e) => e._tag === "RetryableError",
    },
  }
);
The shouldRetry predicate receives the error returned by your catch handler, allowing you to make retry decisions based on enriched error context.

Backoff Strategies

{
  retry: {
    times: 3,
    delayMs: 100,
    backoff: "constant", // 100ms, 100ms, 100ms
  }
}

Type Signature

function tryPromise<A>(
  thunk: () => Promise<A>,
  config?: RetryConfig<UnhandledException>
): Promise<Result<A, UnhandledException>>;

function tryPromise<A, E>(
  options: { 
    try: () => Promise<A>; 
    catch: (cause: unknown) => E | Promise<E> 
  },
  config?: RetryConfig<E>
): Promise<Result<A, E>>;

type RetryConfig<E> = {
  retry?: {
    times: number;
    delayMs: number;
    backoff: "linear" | "constant" | "exponential";
    shouldRetry?: (error: E) => boolean;
  };
};

When to Use Each Method

Use when you already have a success value and want to wrap it in a Result.
const validated = validateInput(data);
return Result.ok(validated);

Next Steps