Result Types Instead of Throwing

Make Failure Visible in the Type

A Result<T, E> return type forces the caller to handle failure before reading the value. Use it for expected failures (invalid input, out of stock, payment declined) and keep exceptions for unexpected bugs.

Example

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 });

type CouponError =
  | { type: "NOT_FOUND" }
  | { type: "EXPIRED"; on: string }
  | { type: "MIN_ORDER"; min: number };

const coupons: Record<string, { percent: number; min: number; expires: string }> = {
  FEST20: { percent: 20, min: 999, expires: "2026-12-31" },
  OLD10: { percent: 10, min: 0, expires: "2025-01-01" },
};

function applyCoupon(code: string, cart: number, today: string): Result<number, CouponError> {
  const c = coupons[code];
  if (!c) return Err({ type: "NOT_FOUND" });
  if (today > c.expires) return Err({ type: "EXPIRED", on: c.expires });
  if (cart < c.min) return Err({ type: "MIN_ORDER", min: c.min });
  return Ok(cart - (cart * c.percent) / 100);
}

for (const [code, cart] of [["FEST20", 1500], ["FEST20", 500], ["OLD10", 800], ["XYZ", 800]] as const) {
  const r = applyCoupon(code, cart, "2026-01-15");
  if (r.ok) console.log(code, "-> pay", r.value);
  else if (r.error.type === "MIN_ORDER") console.log(code, "-> add Rs.", r.error.min - cart, "more");
  else if (r.error.type === "EXPIRED") console.log(code, "-> expired on", r.error.on);
  else console.log(code, "-> invalid coupon");
}

Output

FEST20 -> pay 1200
FEST20 -> add Rs. 499 more
OLD10 -> expired on 2025-01-01
XYZ -> invalid coupon

Forgetting to Check

Example

Reading value without checking ok is an error (TS2339), because the failure branch has no value.

type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
declare function parseAmount(s: string): Result<number, string>;

const total = parseAmount("100").value + 10;

Compiler output (tsc)

Line 4: error TS2339: Property 'value' does not exist on type 'Result<number, string>'.
  Property 'value' does not exist on type '{ ok: false; error: string; }'.

Throw or Return?

Use exceptions forUse Result for
bugs and impossible statesexpected business failures
infrastructure failures (DB down)validation errors shown to users
framework integration (NestJS exception filters turn them into HTTP errors)pure domain functions that are easy to test