Skip to main content
This guide will walk you through the core features of better-result by building a user profile fetcher that handles errors gracefully.

What We’ll Build

We’ll create a function that:
  • Fetches user data from an API
  • Parses and validates the response
  • Handles multiple error types (network, parsing, validation)
  • Uses generator composition for clean control flow
  • Provides type-safe error handling with pattern matching
1

Create Ok and Err Results

Let’s start with the basics — creating success and error Results:
import { Result } from "better-result";

// Success value
const success = Result.ok(42);
console.log(success.value); // 42

// Error value
const error = Result.err("Something went wrong");
console.log(error.error); // "Something went wrong"

// Type guards
if (Result.isOk(success)) {
  console.log("Got value:", success.value);
}

if (Result.isError(error)) {
  console.log("Got error:", error.error);
}
Results use a discriminated union with status: "ok" | "error" for type narrowing.
2

Wrap Throwing Functions

Most JavaScript APIs throw exceptions. Wrap them with Result.try to convert exceptions into type-safe Results:
import { Result } from "better-result";

// Wrap JSON.parse (throws on invalid JSON)
const parsed = Result.try(() => JSON.parse('{"name": "Alice"}'));

if (Result.isOk(parsed)) {
  console.log(parsed.value.name); // "Alice"
} else {
  // error is UnhandledException (contains the original exception)
  console.error("Parse failed:", parsed.error.cause);
}

// Wrap async functions
const response = await Result.tryPromise(() => fetch("https://api.example.com/user/1"));
The error type is UnhandledException by default. We’ll create custom error types next.
3

Define Tagged Errors

Create discriminated error types for exhaustive pattern matching:
import { TaggedError } from "better-result";

class NetworkError extends TaggedError("NetworkError")<{
  url: string;
  message: string;
}>() {}

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

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

// Create error instances
const netErr = new NetworkError({
  url: "https://api.example.com",
  message: "Connection refused",
});

console.log(netErr._tag); // "NetworkError"
console.log(netErr.url);  // "https://api.example.com"
Now we can use custom error handlers in Result.try:
const parsed = Result.try({
  try: () => JSON.parse(input),
  catch: (e) => new ParseError({
    message: e instanceof Error ? e.message : String(e),
  }),
});
// Result<unknown, ParseError>
4

Compose with Result.gen

The real power of better-result comes from generator composition. Use yield* to unwrap Results and automatically short-circuit on errors:
user-profile.ts
import { Result, TaggedError } from "better-result";

// Define our error types
class NetworkError extends TaggedError("NetworkError")<{
  url: string;
  message: string;
}>() {}

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

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

type AppError = NetworkError | ParseError | ValidationError;

// Define types
interface User {
  id: number;
  name: string;
  email: string;
}

// Helper: Fetch with custom error handling
function fetchUser(id: number): Promise<Result<Response, NetworkError>> {
  return Result.tryPromise(
    {
      try: () => fetch(`https://api.example.com/users/${id}`),
      catch: (e) => new NetworkError({
        url: `https://api.example.com/users/${id}`,
        message: e instanceof Error ? e.message : String(e),
      }),
    },
    {
      retry: {
        times: 3,
        delayMs: 100,
        backoff: "exponential",
      },
    },
  );
}

// Helper: Parse JSON response
function parseUserJSON(text: string): Result<unknown, ParseError> {
  return Result.try({
    try: () => JSON.parse(text),
    catch: (e) => new ParseError({
      message: e instanceof Error ? e.message : String(e),
    }),
  });
}

// Helper: Validate user shape
function validateUser(data: unknown): Result<User, ValidationError> {
  if (!data || typeof data !== "object") {
    return Result.err(new ValidationError({
      field: "data",
      message: "Expected object",
    }));
  }

  const obj = data as Record<string, unknown>;

  if (typeof obj.id !== "number") {
    return Result.err(new ValidationError({
      field: "id",
      message: "Expected number",
    }));
  }

  if (typeof obj.name !== "string") {
    return Result.err(new ValidationError({
      field: "name",
      message: "Expected string",
    }));
  }

  if (typeof obj.email !== "string") {
    return Result.err(new ValidationError({
      field: "email",
      message: "Expected string",
    }));
  }

  return Result.ok({ id: obj.id, name: obj.name, email: obj.email });
}

// Compose everything with Result.gen
export async function getUserProfile(id: number): Promise<Result<User, AppError>> {
  return await Result.gen(async function* () {
    // Fetch user (auto-unwraps or short-circuits on error)
    const response = yield* Result.await(fetchUser(id));

    // Check status
    if (!response.ok) {
      return Result.err(new NetworkError({
        url: response.url,
        message: `HTTP ${response.status}`,
      }));
    }

    // Get response text
    const text = yield* Result.await(
      Result.tryPromise({
        try: () => response.text(),
        catch: (e) => new NetworkError({
          url: response.url,
          message: e instanceof Error ? e.message : String(e),
        }),
      }),
    );

    // Parse JSON
    const json = yield* parseUserJSON(text);

    // Validate structure
    const user = yield* validateUser(json);

    return Result.ok(user);
  });
}
yield* unwraps Ok values and short-circuits on Err. Use Result.await() to wrap Promise<Result> in async generators.
5

Handle Errors with Pattern Matching

Now use the function and handle all possible errors:
main.ts
import { getUserProfile } from "./user-profile";
import { matchError } from "better-result";

const result = await getUserProfile(1);

if (result.isOk()) {
  console.log("User:", result.value);
} else {
  // Exhaustive error handling
  const message = matchError(result.error, {
    NetworkError: (e) => `Network error at ${e.url}: ${e.message}`,
    ParseError: (e) => `Failed to parse JSON: ${e.message}`,
    ValidationError: (e) => `Invalid ${e.field}: ${e.message}`,
  });

  console.error(message);
}
matchError ensures all error cases are handled. TypeScript will error if you miss any error types!
Alternatively, use Result.match() for inline pattern matching:
const message = result.match({
  ok: (user) => `Welcome, ${user.name}!`,
  err: (error) => matchError(error, {
    NetworkError: (e) => `Network error: ${e.message}`,
    ParseError: (e) => `Parse error: ${e.message}`,
    ValidationError: (e) => `Validation error on ${e.field}: ${e.message}`,
  }),
});

console.log(message);
6

Transform Results

Use .map() and .andThen() to transform Results:
import { Result } from "better-result";

// Transform success values
const result = Result.ok(2)
  .map((x) => x * 2)      // Ok(4)
  .map((x) => x + 10);    // Ok(14)

console.log(result.value); // 14

// Chain Result-returning functions
function divide(a: number, b: number): Result<number, string> {
  return b === 0 ? Result.err("Division by zero") : Result.ok(a / b);
}

const chained = Result.ok(10)
  .andThen((x) => divide(x, 2))  // Ok(5)
  .andThen((x) => divide(x, 0)); // Err("Division by zero")

if (chained.isErr()) {
  console.error(chained.error); // "Division by zero"
}

// Transform errors
const withMappedError = Result.err("simple error")
  .mapError((e) => ({ code: 500, message: e }));

if (withMappedError.isErr()) {
  console.log(withMappedError.error); // { code: 500, message: "simple error" }
}

Complete Example

Here’s the full working example:
import { Result, TaggedError } from "better-result";

class NetworkError extends TaggedError("NetworkError")<{
  url: string;
  message: string;
}>() {}

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

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

type AppError = NetworkError | ParseError | ValidationError;

interface User {
  id: number;
  name: string;
  email: string;
}

function fetchUser(id: number): Promise<Result<Response, NetworkError>> {
  return Result.tryPromise(
    {
      try: () => fetch(`https://api.example.com/users/${id}`),
      catch: (e) => new NetworkError({
        url: `https://api.example.com/users/${id}`,
        message: e instanceof Error ? e.message : String(e),
      }),
    },
    { retry: { times: 3, delayMs: 100, backoff: "exponential" } },
  );
}

function parseUserJSON(text: string): Result<unknown, ParseError> {
  return Result.try({
    try: () => JSON.parse(text),
    catch: (e) => new ParseError({
      message: e instanceof Error ? e.message : String(e),
    }),
  });
}

function validateUser(data: unknown): Result<User, ValidationError> {
  if (!data || typeof data !== "object") {
    return Result.err(new ValidationError({ field: "data", message: "Expected object" }));
  }
  const obj = data as Record<string, unknown>;
  if (typeof obj.id !== "number") {
    return Result.err(new ValidationError({ field: "id", message: "Expected number" }));
  }
  if (typeof obj.name !== "string") {
    return Result.err(new ValidationError({ field: "name", message: "Expected string" }));
  }
  if (typeof obj.email !== "string") {
    return Result.err(new ValidationError({ field: "email", message: "Expected string" }));
  }
  return Result.ok({ id: obj.id, name: obj.name, email: obj.email });
}

export async function getUserProfile(id: number): Promise<Result<User, AppError>> {
  return await Result.gen(async function* () {
    const response = yield* Result.await(fetchUser(id));
    if (!response.ok) {
      return Result.err(new NetworkError({ url: response.url, message: `HTTP ${response.status}` }));
    }
    const text = yield* Result.await(
      Result.tryPromise({
        try: () => response.text(),
        catch: (e) => new NetworkError({
          url: response.url,
          message: e instanceof Error ? e.message : String(e),
        }),
      }),
    );
    const json = yield* parseUserJSON(text);
    const user = yield* validateUser(json);
    return Result.ok(user);
  });
}

What You’ve Learned

You now know how to:
  • ✅ Create Ok and Err results
  • ✅ Wrap throwing functions with Result.try and Result.tryPromise
  • ✅ Define custom error types with TaggedError
  • ✅ Compose operations with Result.gen and yield*
  • ✅ Handle errors exhaustively with pattern matching
  • ✅ Transform Results with .map() and .andThen()

Next Steps

Result Type Deep Dive

Learn about all Result methods and combinators

Error Handling Patterns

Master TaggedError and error composition patterns

Generator Composition

Understand how Result.gen works under the hood

API Reference

Explore the complete API documentation

Build docs developers (and LLMs) love