AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

11 Functional patterns / Lesson 5 of 5

Maybe, Result and railway-oriented code — making absence and failure values

13 min · +10 XP · 2 exercises · 5 questions

Read the text13 min
Exercises0/2
Quiz5 q

Quick answer

What are Maybe and Result types?

Maybe and Result are small wrapper types that make absence and failure explicit values instead of undefined or a thrown exception. A Maybe is Some(value) or None; a Result is Ok(value) or Err(error), and it records why a step failed. Both offer map and flatMap, so a chain of steps that may each fail reads as a straight line.

undefined and exceptions are JavaScript's answers to "there is no value" and "this failed", and both are invisible in a function's signature: nothing tells the caller that findUser may return undefined or that parseConfig may throw. Two small wrapper types make those outcomes explicit values: Maybe (a value that may be absent) and Result (a success or a typed failure). With map and flatMap on them, a chain of steps that might each fail becomes a straight line — the "railway": success rides one track, the first failure switches to the other and skips the rest. This lesson builds both types in a few dozen lines, shows the railway, relates them to things you already use (?., ??, arrays, promises), and is frank about when plain JavaScript idioms are the better choice.

Maybe

const Some = (value) => ({
  isSome: true,
  map: (f) => Some(f(value)),
  flatMap: (f) => f(value),                    // f returns a Maybe — avoids Some(Some(x))
  filter: (p) => (p(value) ? Some(value) : None),
  getOrElse: () => value,
  toString: () => `Some(${JSON.stringify(value)})`,
});
const None = {
  isSome: false,
  map: () => None, flatMap: () => None, filter: () => None,
  getOrElse: (fallback) => fallback,
  toString: () => "None",
};
const fromNullable = (v) => (v === null || v === undefined ? None : Some(v));

const city = fromNullable(users.find((u) => u.id === id))
  .flatMap((u) => fromNullable(u.address))
  .map((a) => a.city.toUpperCase())
  .getOrElse("unknown");

map transforms the value if present; flatMap chains a step that itself may produce absence; None short-circuits everything; getOrElse leaves the type at the end. Compare with the built-in: users.find(…)?.address?.city?.toUpperCase() ?? "unknown". For a chain of property accesses, optional chaining wins — shorter, standard, no allocation. Maybe earns its place when steps are functions that may return nothing (lookups, parses, finds), when you want to filter in the middle, or when you pass the possibly-absent value around before deciding what to do with it.

Result

const Ok = (value) => ({
  ok: true, value,
  map: (f) => Ok(f(value)),
  mapErr: () => Ok(value),
  flatMap: (f) => f(value),
  getOrElse: () => value,
  match: ({ ok }) => ok(value),
});
const Err = (error) => ({
  ok: false, error,
  map: () => Err(error),
  mapErr: (f) => Err(f(error)),
  flatMap: () => Err(error),
  getOrElse: (fallback) => fallback,
  match: ({ err }) => err(error),
});
const attempt = (fn) => { try { return Ok(fn()); } catch (e) { return Err(e); } };   // exceptions → Result at the boundary

Result carries why it failed, which Maybe cannot. map transforms a success; mapErr transforms a failure (add context, translate to a user message); flatMap chains a step that may fail; match forces the caller to handle both cases. attempt wraps a throwing function — the adapter between exception-based code (JSON.parse) and Result-based code.

The railway

const parseJson = (text) => attempt(() => JSON.parse(text)).mapErr(() => "not valid JSON");
const requireObject = (v) => (v !== null && typeof v === "object" && !Array.isArray(v) ? Ok(v) : Err("config must be an object"));
const requirePort = (cfg) => (Number.isInteger(cfg.port) && cfg.port > 0 && cfg.port < 65536 ? Ok(cfg) : Err(`port must be 1-65535, got ${JSON.stringify(cfg.port)}`));
const withDefaults = (cfg) => ({ retries: 3, ...cfg });

const loadConfig = (text) => parseJson(text).flatMap(requireObject).flatMap(requirePort).map(withDefaults);

loadConfig('{"port":8080}').match({ ok: (cfg) => start(cfg), err: (msg) => console.error(msg) });

Each step is a small pure function; flatMap connects those that can fail, map those that cannot; the first Err rides the failure track to the end untouched, so no step needs an if (!ok) return. Errors are values: collect them, count them, log them, return them from an API. Module 7's result objects ({ ok, value } | { ok, error }) are this pattern without the methods; adding map/flatMap is what removes the if (!r.ok) return r from every line.

Collecting many results

Result short-circuits at the first failure. For a form or a batch you want all failures: fold the results — const errors = results.filter((r) => !r.ok).map((r) => r.error); if none, Ok(results.map((r) => r.value)). That is a sequence/traverse in functional vocabulary; write it as a plain reduce. Promise.allSettled is the async version of the same idea.

You already use these shapes

  • Arrays have map/flatMap: an array is "zero or more values" and flatMap chains steps that return arrays.
  • Promises are Result for the future: then is map/flatMap (returned promises flatten), catch is mapErr+recovery, a rejection rides the failure track past every then.
  • ?./?? are a built-in Maybe for property paths.

The word for a type with a lawful map is functor; with flatMap (chain, bind) and a constructor (Some, Ok, Promise.resolve), monad. The laws just say map(identity) changes nothing and chaining composes associatively — properties you can check in a test and otherwise ignore. You do not need the vocabulary to use the pattern well.

Where to draw the line

Use Maybe/Result when: a pipeline has several steps that may each fail or return nothing; failures must be data (APIs, validation reports, batch jobs); or you want the signature to say that absence/failure is possible (TypeScript makes this compelling — Result<Config, ConfigError>). Prefer plain idioms when: a chain is property access (?.), a single call may fail (try/catch at the boundary), or the code is a script whose failure should simply stop it. A codebase where every function returns Result and every call site unwraps it has replaced exceptions with ceremony; the railway is for the stretches where several fallible steps line up.

Common mistakes

  • map where flatMap was needed — Some(Some(x)), Ok(Ok(x)), a Result inside a Result.
  • Unwrapping (getOrElse, .value) in the middle of a chain instead of at the end.
  • Losing the error: converting Err to None, or mapErr to a message too early to distinguish causes.
  • Mixing exceptions and Results inside one layer — convert at the boundary with attempt.
  • Building a Maybe for a three-level property access that ?. already handles.
  • Making the types heavier (classes, prototypes, generics) than the problem needs.

Interview angle

  • "What problem do Maybe/Result solve?" They make absence and failure explicit values with map/flatMap, so chains of fallible steps read as straight lines and callers must handle both outcomes.
  • "map versus flatMap on these?" map applies a plain function to the inner value; flatMap applies a function that itself returns the wrapper, avoiding nesting.
  • "How do promises relate?" A promise is a Result over time — then maps/chains, rejection is the failure track.
  • "When is optional chaining better than Maybe?" For property paths; Maybe for chaining functions that may return nothing.
  • "How do you collect all errors instead of stopping at the first?" Fold the results: partition into failures and successes (sequence/traverse).

Key takeaways

  • Maybe = Some(v) | None with map/flatMap/filter/getOrElse; Result = Ok(v) | Err(e) with map/mapErr/flatMap/match; attempt converts throws.
  • Railway: flatMap for fallible steps, map for safe ones; the first failure skips the rest; unwrap once at the end.
  • Collect all failures with a fold when partial results matter.
  • Arrays, promises and ?./?? are the same shapes you already use; the laws are just sanity checks.
  • Use the pattern for stretches of several fallible steps and for failures-as-data; plain idioms elsewhere.

Common questions

What is railway-oriented programming?

Railway-oriented programming chains steps that return a Result with flatMap, so success rides one track and the first Err switches to the other and skips every remaining step untouched. No step needs an if (!ok) return, the error arrives at the end as a value, and one match there handles both outcomes.

What is the difference between map and flatMap on Maybe or Result?

map applies a plain function to the wrapped value and wraps the result again. flatMap applies a function that itself returns a Maybe or Result and does not wrap it a second time. Using map with a fallible step produces Some(Some(x)) or Ok(Ok(x)), the sign that flatMap was needed.

When is optional chaining better than a Maybe type?

For a chain of property accesses: user?.address?.city ?? "unknown" is shorter, standard and allocates nothing. A Maybe earns its place when the steps are functions that may return nothing, such as lookups, parses and find calls, when you want to filter midway, or when the possibly absent value is passed around before a decision.

How are promises related to the Result type?

A promise is a Result spread over time. then works like map and flatMap, since a returned promise is flattened, catch acts as mapErr plus recovery, and a rejection rides the failure track past every then until something catches it.

How do you collect all validation errors instead of stopping at the first?

Run every check to get a list of results, then fold them: gather the errors from every failed result, and if there are none, return Ok with all the values. Functional libraries call this sequence or traverse; Promise.allSettled is the asynchronous version of the same idea.

Mark as readSign in to keep your place.

Maybe and Result in JavaScript: Railway-Oriented Programming

Maybe and Result make absence and failure explicit values. Build both in JavaScript, chain fallible steps with flatMap, and see when optional chaining wins.

What are Maybe and Result types?

Maybe and Result are small wrapper types that make absence and failure explicit values instead of undefined or a thrown exception. A Maybe is Some(value) or None; a Result is Ok(value) or Err(error), and it records why a step failed. Both offer map and flatMap, so a chain of steps that may each fail reads as a straight line.

Lesson

undefined and exceptions are JavaScript's answers to "there is no value" and "this failed", and both are invisible in a function's signature: nothing tells the caller that findUser may return undefined or that parseConfig may throw. Two small wrapper types make those outcomes explicit values: Maybe (a value that may be absent) and Result (a success or a typed failure). With map and flatMap on them, a chain of steps that might each fail becomes a straight line — the "railway": success rides one track, the first failure switches to the other and skips the rest. This lesson builds both types in a few dozen lines, shows the railway, relates them to things you already use (?., ??, arrays, promises), and is frank about when plain JavaScript idioms are the better choice.

Maybe

const Some = (value) => ({
  isSome: true,
  map: (f) => Some(f(value)),
  flatMap: (f) => f(value),                    // f returns a Maybe — avoids Some(Some(x))
  filter: (p) => (p(value) ? Some(value) : None),
  getOrElse: () => value,
  toString: () => `Some(${JSON.stringify(value)})`,
});
const None = {
  isSome: false,
  map: () => None, flatMap: () => None, filter: () => None,
  getOrElse: (fallback) => fallback,
  toString: () => "None",
};
const fromNullable = (v) => (v === null || v === undefined ? None : Some(v));

const city = fromNullable(users.find((u) => u.id === id))
  .flatMap((u) => fromNullable(u.address))
  .map((a) => a.city.toUpperCase())
  .getOrElse("unknown");

map transforms the value if present; flatMap chains a step that itself may produce absence; None short-circuits everything; getOrElse leaves the type at the end. Compare with the built-in: users.find(…)?.address?.city?.toUpperCase() ?? "unknown". For a chain of property accesses, optional chaining wins — shorter, standard, no allocation. Maybe earns its place when steps are functions that may return nothing (lookups, parses, finds), when you want to filter in the middle, or when you pass the possibly-absent value around before deciding what to do with it.

Result

const Ok = (value) => ({
  ok: true, value,
  map: (f) => Ok(f(value)),
  mapErr: () => Ok(value),
  flatMap: (f) => f(value),
  getOrElse: () => value,
  match: ({ ok }) => ok(value),
});
const Err = (error) => ({
  ok: false, error,
  map: () => Err(error),
  mapErr: (f) => Err(f(error)),
  flatMap: () => Err(error),
  getOrElse: (fallback) => fallback,
  match: ({ err }) => err(error),
});
const attempt = (fn) => { try { return Ok(fn()); } catch (e) { return Err(e); } };   // exceptions → Result at the boundary

Result carries why it failed, which Maybe cannot. map transforms a success; mapErr transforms a failure (add context, translate to a user message); flatMap chains a step that may fail; match forces the caller to handle both cases. attempt wraps a throwing function — the adapter between exception-based code (JSON.parse) and Result-based code.

The railway

const parseJson = (text) => attempt(() => JSON.parse(text)).mapErr(() => "not valid JSON");
const requireObject = (v) => (v !== null && typeof v === "object" && !Array.isArray(v) ? Ok(v) : Err("config must be an object"));
const requirePort = (cfg) => (Number.isInteger(cfg.port) && cfg.port > 0 && cfg.port < 65536 ? Ok(cfg) : Err(`port must be 1-65535, got ${JSON.stringify(cfg.port)}`));
const withDefaults = (cfg) => ({ retries: 3, ...cfg });

const loadConfig = (text) => parseJson(text).flatMap(requireObject).flatMap(requirePort).map(withDefaults);

loadConfig('{"port":8080}').match({ ok: (cfg) => start(cfg), err: (msg) => console.error(msg) });

Each step is a small pure function; flatMap connects those that can fail, map those that cannot; the first Err rides the failure track to the end untouched, so no step needs an if (!ok) return. Errors are values: collect them, count them, log them, return them from an API. Module 7's result objects ({ ok, value } | { ok, error }) are this pattern without the methods; adding map/flatMap is what removes the if (!r.ok) return r from every line.

Collecting many results

Result short-circuits at the first failure. For a form or a batch you want all failures: fold the results — const errors = results.filter((r) => !r.ok).map((r) => r.error); if none, Ok(results.map((r) => r.value)). That is a sequence/traverse in functional vocabulary; write it as a plain reduce. Promise.allSettled is the async version of the same idea.

You already use these shapes

  • Arrays have map/flatMap: an array is "zero or more values" and flatMap chains steps that return arrays.
  • Promises are Result for the future: then is map/flatMap (returned promises flatten), catch is mapErr+recovery, a rejection rides the failure track past every then.
  • ?./?? are a built-in Maybe for property paths.

The word for a type with a lawful map is functor; with flatMap (chain, bind) and a constructor (Some, Ok, Promise.resolve), monad. The laws just say map(identity) changes nothing and chaining composes associatively — properties you can check in a test and otherwise ignore. You do not need the vocabulary to use the pattern well.

Where to draw the line

Use Maybe/Result when: a pipeline has several steps that may each fail or return nothing; failures must be data (APIs, validation reports, batch jobs); or you want the signature to say that absence/failure is possible (TypeScript makes this compelling — Result<Config, ConfigError>). Prefer plain idioms when: a chain is property access (?.), a single call may fail (try/catch at the boundary), or the code is a script whose failure should simply stop it. A codebase where every function returns Result and every call site unwraps it has replaced exceptions with ceremony; the railway is for the stretches where several fallible steps line up.

Common mistakes

  • map where flatMap was needed — Some(Some(x)), Ok(Ok(x)), a Result inside a Result.
  • Unwrapping (getOrElse, .value) in the middle of a chain instead of at the end.
  • Losing the error: converting Err to None, or mapErr to a message too early to distinguish causes.
  • Mixing exceptions and Results inside one layer — convert at the boundary with attempt.
  • Building a Maybe for a three-level property access that ?. already handles.
  • Making the types heavier (classes, prototypes, generics) than the problem needs.

Interview angle

  • "What problem do Maybe/Result solve?" They make absence and failure explicit values with map/flatMap, so chains of fallible steps read as straight lines and callers must handle both outcomes.
  • "map versus flatMap on these?" map applies a plain function to the inner value; flatMap applies a function that itself returns the wrapper, avoiding nesting.
  • "How do promises relate?" A promise is a Result over time — then maps/chains, rejection is the failure track.
  • "When is optional chaining better than Maybe?" For property paths; Maybe for chaining functions that may return nothing.
  • "How do you collect all errors instead of stopping at the first?" Fold the results: partition into failures and successes (sequence/traverse).

Key takeaways

  • Maybe = Some(v) | None with map/flatMap/filter/getOrElse; Result = Ok(v) | Err(e) with map/mapErr/flatMap/match; attempt converts throws.
  • Railway: flatMap for fallible steps, map for safe ones; the first failure skips the rest; unwrap once at the end.
  • Collect all failures with a fold when partial results matter.
  • Arrays, promises and ?./?? are the same shapes you already use; the laws are just sanity checks.
  • Use the pattern for stretches of several fallible steps and for failures-as-data; plain idioms elsewhere.

Common questions

What is railway-oriented programming?

Railway-oriented programming chains steps that return a Result with flatMap, so success rides one track and the first Err switches to the other and skips every remaining step untouched. No step needs an if (!ok) return, the error arrives at the end as a value, and one match there handles both outcomes.

What is the difference between map and flatMap on Maybe or Result?

map applies a plain function to the wrapped value and wraps the result again. flatMap applies a function that itself returns a Maybe or Result and does not wrap it a second time. Using map with a fallible step produces Some(Some(x)) or Ok(Ok(x)), the sign that flatMap was needed.

When is optional chaining better than a Maybe type?

For a chain of property accesses: user?.address?.city ?? "unknown" is shorter, standard and allocates nothing. A Maybe earns its place when the steps are functions that may return nothing, such as lookups, parses and find calls, when you want to filter midway, or when the possibly absent value is passed around before a decision.

How are promises related to the Result type?

A promise is a Result spread over time. then works like map and flatMap, since a returned promise is flattened, catch acts as mapErr plus recovery, and a rejection rides the failure track past every then until something catches it.

How do you collect all validation errors instead of stopping at the first?

Run every check to get a list of results, then fold them: gather the errors from every failed result, and if there are none, return Ok with all the values. Functional libraries call this sequence or traverse; Promise.allSettled is the asynchronous version of the same idea.

Exercises

Chaining Maybes

Using the starter's Some/None/fromNullable, for each id on the input lines: find the user (fromNullable), flatMap to its address, map the city to upper case, filter cities longer than 3 characters, and print <id>: maybe=<String(maybe)> value=<getOrElse("unknown")> optionalChaining=<the same lookup with ?. and ??> name=<user's name via Maybe or nobody>. Finally print nested: map -> Some containing <Some(2).map((x) => Some(x)).getOrElse()> | flatMap -> <Some(2).flatMap((x) => Some(x))>.

Example: users [{"id":1,"name":"Ada","address":{"city":"London"}},{"id":2,"name":"Bob"},{"id":3,"name":"Cy","address":{"city":"Rio"}}] then 1, 2, 3, 9 →

1: maybe=Some("LONDON") value=LONDON optionalChaining=LONDON name=Ada
2: maybe=None value=unknown optionalChaining=unknown name=Bob
3: maybe=None value=unknown optionalChaining=RIO name=Cy
9: maybe=None value=unknown optionalChaining=unknown name=nobody
nested: map -> Some containing Some(2) | flatMap -> Some(2)

filter is what the Maybe chain can do that optional chaining cannot — Rio is too short.

A Result railway

With the starter's Ok/Err/attempt, build loadUser(text) = parseJson → requireObject → requireName → requireAge → normalise: parseJson wraps JSON.parse with attempt and maps the error to "not valid JSON"; requireObject rejects non-objects with "expected an object"; requireName needs a non-blank string ("name is required"); requireAge needs an integer 0–150 (age must be an integer 0-150, got <JSON>); normalise returns { name: trimmed lower-case, age, adult: age >= 18 }. Print <i>: ok <JSON> or <i>: err <message> per line, then a sequence over all results: all ok: <n> users or <n> failed: <messages joined by " | ">, then recovered=<first result's name or anonymous> mapErrDemo=<Err("x").mapErr(upper).error>.

Example: {"name":" Ada ","age":36}, {"name":"Bob","age":-1}, not json, [1,2], {"age":5} →

0: ok {"name":"ada","age":36,"adult":true}
1: err age must be an integer 0-150, got -1
2: err not valid JSON
3: err expected an object
4: err name is required
4 failed: age must be an integer 0-150, got -1 | not valid JSON | expected an object | name is required
recovered=ada mapErrDemo=X

In this module: Functional patterns

← Memoization and laziness — computing once, and only when asked · Checkpoint — Functional patterns →