DEV Community

Khafido Ilzam
Khafido Ilzam

Posted on

Result Pattern

In modern software engineering, how we handle "failure" defines the resilience and readability of our systems. For years, the default mechanism has been Exceptions. However, as systems grow in complexity, many senior architects are shifting toward the Result Pattern.

The Result Pattern treats errors not as "exceptional" interruptions to the program flow, but as first-class data. It forces the developer to acknowledge the possibility of failure at compile-time, leading to safer, more predictable codebases.


What is the Result Pattern?

At its core, the Result Pattern is a design pattern that wraps the output of a function in a container. This container represents one of two states:

  1. Success: Contains the expected data.
  2. Failure: Contains an error object or message explaining what went wrong.

Instead of a function potentially "exploding" with a thrown exception, it returns a value that says, "I might have worked, or I might have failed—here is the result for you to inspect."


The Core Advantages

1. Explicit Intent

With standard exceptions, a function signature like getUser(id: string): User is a lie. It doesn't always return a User; it might throw a 404 or a DatabaseError. The Result Pattern makes this honest: getUser(id: string): Result<User, Error>.

2. Type Safety & Exhaustive Checking

In languages like TypeScript, the compiler can force you to check if the result is a success before accessing the data. This eliminates the "forgot to catch" bugs that haunt production logs.

3. Better Control Flow

Exceptions are essentially goto statements in disguise—they jump across the call stack, making logic hard to follow. Results keep the execution local and linear.


Trade-offs and Considerations

No pattern is a silver bullet. Here is the "senior" perspective on the downsides:

  • Boilerplate: You will find yourself writing more if (result.isFailure) checks. While this is safer, it can feel verbose compared to a single try-catch block wrapping ten lines of code.
  • Learning Curve: Team members used to traditional OOP might find the functional nature of Result objects (and the lack of "bubbling" errors) counterintuitive at first.
  • Performance: In highly high-frequency loops, creating a new object for every single operation (even successful ones) can add a negligible but measurable overhead compared to raw values.

Implementation in TypeScript

Here is a robust, production-ready implementation of the Result Pattern.

type Success<T> = {
  readonly isSuccess: true;
  readonly isFailure: false;
  readonly value: T;
};

type Failure<E> = {
  readonly isSuccess: false;
  readonly isFailure: true;
  readonly error: E;
};

export type Result<T, E = Error> = Success<T> | Failure<E>;

/**
 * Functional helpers to create Result instances
 */
export const ok = <T>(value: T): Success<T> => ({
  isSuccess: true,
  isFailure: false,
  value,
});

export const fail = <E>(error: E): Failure<E> => ({
  isSuccess: false,
  isFailure: true,
  error,
});

Enter fullscreen mode Exit fullscreen mode

Real-World Usage Example

Let’s look at a service that fetches a user. Notice how the type system protects us from accessing result.value until we verify success.

interface User {
  id: string;
  username: string;
}

async function findUser(id: string): Promise<Result<User, string>> {
  const user = await db.users.findOne(id);

  if (!user) {
    return fail("User not found in database.");
  }

  return ok(user);
}

// Consuming the Result
async function userController(userId: string) {
  const result = await findUser(userId);

  if (result.isFailure) {
    // TypeScript knows 'error' exists here, but 'value' does not
    return console.error(`Error: ${result.error}`);
  }

  // TypeScript now safely allows access to 'value'
  console.log(`Hello, ${result.value.username}`);
}

Enter fullscreen mode Exit fullscreen mode

When to use it?

Use the Result Pattern when... Use Exceptions when...
The error is expected (e.g., validation, user not found). The error is catastrophic (e.g., out of memory, lost DB connection).
You are writing Domain Logic. You are in the Infrastructure layer.
You want to ensure the caller handles the error. You want the error to bubble up to a global handler.

Final Verdict

The Result Pattern is about moving from defensive programming (hoping nothing breaks) to offensive programming (designing for failure). By making errors a part of your type system, you create a codebase that is self-documenting and significantly more robust. For any serious TypeScript project, it is a pattern worth adopting.

Top comments (0)