Result & tryCatch

Naseebullah Ahmadi  Senior Software Engineer, London

A `Result<T, E>` union plus `tryCatch` wrappers that turn a throwing call into a value the caller has to inspect: the failure path becomes part of the return type instead of an invisible jump.

3 min read
#tooling

A thrown exception is invisible in a function's type - nothing at the call site says it can fail, and catch (e) just hands you unknown. Result<T, E> moves the failure into the return type instead: the caller gets { ok: true, value } or { ok: false, error }, and can't reach value without checking ok first.

@itsnas usage.ts
codeusage.ts
// Wrap a sync call that throws, no try/catch at the call site
// @src/code/result
const config = tryCatch(() => JSON.parse(raw) as Config)
 
if (!config.ok) {
  logger.warn('invalid config, using defaults', config.error)
  return defaultConfig
}
 
applyConfig(config.value) // Config, narrowed
 
// Wrap a promise that may reject
const user = await tryCatchAsync(fetchUser(id))
if (!user.ok) return null
return user.value
 
// `error` is always an Error, so subclasses still narrow
const insert = await tryCatchAsync(db.insert(row))
if (!insert.ok && insert.error instanceof UniqueViolationError) {
  return conflict()
}
 
// For a failure you raise yourself, return ok() / err() directly
function half(n: number): Result<number, string> {
  return n % 2 === 0 ? ok(n / 2) : err('odd number')
}
main
Nas (@itsnas)