Skip to content
Chapter 08 · 15 minutes

Errors as values

Make failure part of the signature, so nobody can forget to handle it.

Goal

Return expected failures as values with a Result type, and keep throw for the truly unexpected.

The problem with throw

function checkIn(...): Habit says nothing about failing. If it throws, every caller must remember a try, and the checker cannot remind them.

Result

From streak/src/result.ts:

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

export function ok<T>(value: T): Result<T, never> {
  return { ok: true, value }
}

export function err<E>(error: E): Result<never, E> {
  return { ok: false, error }
}

It is just a discriminated union (chapter 05) with generics (chapter 06).

Using it

checkIn in streak/src/habits.ts says in its type that it can fail, and how:

export function checkIn(habit: Habit, day: Day, today: Day): Result<Habit, CheckInError> {
  if (day > today) return err({ kind: 'future', day })
  if (day < habit.createdOn) return err({ kind: 'before-start', day, createdOn: habit.createdOn })
  if (habit.checkIns.includes(day)) return err({ kind: 'duplicate', day })
  return ok({ ...habit, checkIns: [...habit.checkIns, day].sort() })
}

The caller cannot reach value without checking ok first. From streak/src/cli.ts:

const checked = checkIn(habit, when, today)
if (!checked.ok) return fail(explain(checked.error))
await store.save(habits.map((h) => (h.id === habit.id ? checked.value : h)))

Try deleting the if line: checked.value turns red.

When to throw anyway

Use Result for expected failures a user can cause (bad input, a duplicate). Keep throw for bugs and broken environments (a disk you cannot read). fileStore does exactly that: a missing file is fine, invalid JSON is a Result error, and any other read error is thrown.

Check it works

npm run streak -- check read --day 2099-01-01

You get 2099-01-01 has not happened yet. and exit code 1.

Exercise

Change addHabit to return a union of error kinds instead of a plain string, like CheckInError, with an explain for it.