Introduction
Checking if (result.ok) at every step creates deeply nested code. The map/flatMap pattern lets you chain operations on Result values — transforming successes while automatically propagating errors — producing flat, readable pipelines.
Key Concepts
- map: Transforms the success value inside a Result without unwrapping it. Errors pass through unchanged.
- flatMap (chain): Like map, but the transformation itself returns a Result, avoiding nested
Result<Result<T>>. - mapError: Transforms the error value, useful for converting low-level errors into domain errors.
Real World Context
A request handler that parses a body, validates fields, looks up a user, and saves to a database involves four operations that can each fail. Without chaining, you write four nested if blocks. With flatMap, it becomes a flat pipeline.
Deep Dive
Implementing Map and FlatMap
typescripttype Result<T, E> = { ok: true; value: T } | { ok: false; error: E }; function map<T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E> { return result.ok ? { ok: true, value: fn(result.value) } : result; } function flatMap<T, U, E>(result: Result<T, E>, fn: (value: T) => Result<U, E>): Result<U, E> { return result.ok ? fn(result.value) : result; } function mapError<T, E, F>(result: Result<T, E>, fn: (error: E) => F): Result<T, F> { return result.ok ? result : { ok: false, error: fn(result.error) }; }
Chaining Operations
typescriptfunction parseId(raw: string): Result<number, string> { const n = parseInt(raw, 10); return isNaN(n) ? { ok: false, error: "Invalid ID" } : { ok: true, value: n }; } function findUser(id: number): Result<User, string> { const user = db.get(id); return user ? { ok: true, value: user } : { ok: false, error: "User not found" }; } function getUserEmail(rawId: string): Result<string, string> { return map( flatMap(parseId(rawId), findUser), (user) => user.email ); }
The chain reads: parse the ID, then find the user, then extract the email. If any step fails, the error propagates to the final result without additional if checks.
Pipe-style chaining
For better readability, combine with a pipe function:
typescriptconst result = pipe( parseId("42"), (r) => flatMap(r, findUser), (r) => map(r, (user) => user.email) );
Common Pitfalls
- Using map when you need flatMap — If your transformation returns a Result, use flatMap. Using map would produce
Result<Result<T>>, which is unusable. - Ignoring the error type — When chaining operations with different error types, you need to unify them (e.g., with
mapError) or use a common error union.
Best Practices
- Keep individual steps small — Each function in the chain should do one thing: parse, validate, lookup, or transform.
- Use a library for production code — Libraries like
neverthrow,fp-ts, orEffectprovide battle-tested Result/Either implementations with rich chaining APIs.
Summary
maptransforms success values; errors pass through unchanged.flatMapchains operations that themselves return Results, keeping the pipeline flat.mapErrortransforms error values for domain-level error conversion.- For production code, consider
neverthroworEffectinstead of hand-rolling.
Code Examples
typescript
type Result<T, E> = { 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 });
const flatMap = <T, U, E>(r: Result<T, E>, fn: (v: T) => Result<U, E>): Result<U, E> =>
r.ok ? fn(r.value) : r;
const map = <T, U, E>(r: Result<T, E>, fn: (v: T) => U): Result<U, E> =>
r.ok ? ok(fn(r.value)) : r;
// Chain: parse → validate → transform
const result = flatMap(
flatMap(ok("42"), s => {
const n = Number(s);
return isNaN(n) ? err("NaN") : ok(n);
}),
n => n > 0 ? ok(n * 2) : err("Not positive")
);
// result: { ok: true, value: 84 }