Result Type Async Error Handling

Replace thrown exceptions with a Result union for expected failures, chaining async operations without try-catch pyramids or lost errors.

TypescriptResilience

Code

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

const ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
const err = <E>(error: E): Result<never, E> => ({ ok: false, error });

class ValidationError {
  constructor(readonly field: string, readonly reason: string) {}
}

async function fetchUser(id: string): Promise<Result<{ id: string; email: string }, Error>> {
  if (!/^[a-z0-9-]+$/.test(id)) {
    return err(new ValidationError('id', 'invalid characters'));
  }
  const res = await fetch('https://api.example.com/users/' + id);
  if (!res.ok) {
    return err(new Error('upstream status ' + res.status));
  }
  return ok(await res.json());
}

async function mapResult<T, U, E>(
  result: Result<T, E>, fn: (value: T) => Promise<U>,
): Promise<Result<U, E>> {
  if (!result.ok) return result;
  return ok(await fn(result.value));
}

const user = await fetchUser('u-123');
const decorated = await mapResult(user, async (u) => ({ ...u, email: u.email.toUpperCase() }));

if (decorated.ok) {
  console.log('email:', decorated.value.email);
} else if (decorated.error instanceof ValidationError) {
  console.log('field problem:', decorated.error.field);
} else {
  console.log('failure:', decorated.error.message);
}