Error Handling in TypeScript with Result Types

TypeScript Error Handling in TypeScript with Result Types

Move beyond try/catch and embrace safer, more maintainable error handling using the Result type pattern.

TypeScript’s traditional try/catch mechanisms often leave error handling optional and runtime prone to unhandled exceptions. The Result type pattern enforces explicit handling, reducing surprises and improving code reliability.

đź§  What Is a Result Type?

A Result type is a discriminated union that clearly expresses success or failure via structured return values:

type Result<T, E = Error>
  = { ok: true; data: T }
  | { ok: false; error: E };

Instead of throwing, functions return an object with either a data payload or an error, forcing callers to handle both paths explicitly.

âś… Example: Fetching a User Safely

function getUser(id: number): Result<User> {
  if (id === 0) {
    return { ok: false, error: new Error('User not found') };
  }
  return { ok: true, data: { id, name: 'Sample User' } };
}

const result = getUser(0);
if (result.ok) {
  console.log(result.data.name);
} else {
  console.error(result.error.message);
}

This removes implicit exceptions—every outcome is handled.

🔄 Benefits of the Result Pattern

  • Type-safety: TypeScript ensures exhaustive checks on success and failure cases.
  • No hidden exceptions: Errors are values, not thrown surprises.
  • Clear intention: Function signatures show exactly what can go wrong.
  • Consistency: All functions return in a predictable format—no guesswork needed.

⚙️ Advanced Handling with Libraries

For larger applications, consider using libraries like neverthrow, which provide:

  • Functional patterns like map, mapErr, and match
  • Async-friendly APIs like AsyncResult
  • Better integration with functional programming tools
  • Integration with ESLint for enforcing return checking

đź”§ Type Safety & Error Classification

Define custom error types with discriminants to enable smart type narrowing and more descriptive failure states:

class NotFoundError extends Error {
  readonly type = 'not-found';
}

class PermissionDeniedError extends Error {
  readonly type = 'permission-denied';
}

type CustomError = NotFoundError | PermissionDeniedError;

function lookup(): Result<Data, CustomError> {
  return { ok: false, error: new NotFoundError('Missing item') };
}

const res = lookup();
if (res.ok) {
  // handle success
} else if (res.error.type === 'not-found') {
  console.warn('Item not found:', res.error.message);
} else if (res.error.type === 'permission-denied') {
  alert('Access denied');
}

đź§© Integrating with Async Code

Pair Result with async logic for safer data fetching and better control of network behavior:

async function fetchData(): Promise<Result<Data>> {
  try {
    const resp = await fetch('/data');
    const json = await resp.json();
    return { ok: true, data: json };
  } catch (e) {
    return {
      ok: false,
      error: e instanceof Error ? e : new Error(String(e))
    };
  }
}

đź§Ş Testing Result-Based Functions

Testing functions that return Result types is simpler than testing ones that throw:

test('returns data on success', () => {
  const result = getUser(1);
  expect(result.ok).toBe(true);
  if (result.ok) {
    expect(result.data.name).toBe('Sample User');
  }
});

test('returns error on failure', () => {
  const result = getUser(0);
  expect(result.ok).toBe(false);
  if (!result.ok) {
    expect(result.error.message).toBe('User not found');
  }
});

🎨 Integrating with React UI

You can directly use Result types in components for conditional rendering:

const { ok, data, error } = await fetchData();

return (
  <>
    {ok ? (
      <div>Welcome, {data.name}</div>
    ) : (
      <div className="error">Error: {error.message}</div>
    )}
  </>
);

🛡 Best Practices

  • Prefer Result<T, E> over throwing exceptions for expected errors.
  • Define custom error types for clarity and maintainability.
  • Use utility libraries like neverthrow or oxide.ts to avoid boilerplate.
  • Centralize your Result handling logic in shared service layers.
  • Use exhaustive checks in your UI and API layers to prevent edge cases.

📌 Takeaways

  • Use Result types to make error cases explicit and type-safe.
  • Avoid hidden exceptions with structured return values.
  • Adopt libraries like neverthrow for chaining, async support, and enforcement.
  • Improve testing, UI handling, and long-term maintainability with predictable return types.

At Web Expert Solution, we champion clean, maintainable TypeScript patterns. Subscribe for more on robust architectures, progressive design patterns, and practical developer workflows!

Leave a comment

Your email address will not be published. Required fields are marked *

1 × 1 =