Errors that know where, why, and with what data.

Result types for TypeScript. Every failure is a Fault carrying tag, details, location, metadata and cause, and it serialises straight into Sentry or OTel.

bun add @thourum/fault
import { ok, err, retry, Fault, ServiceError } from '@thourum/fault'
import { safeFetchJSON } from '@thourum/fault/fetch'
import { safeZodParse } from '@thourum/fault/zod'

Fault.onCapture = (f) => Sentry.captureException(f, { extra: f.toJSON() })

const me = () => safeFetchJSON('https://api.example.com/me')
const user = await retry(me, { times: 3, delayMs: 200 })
  .andThen(safeZodParse(userSchema))
  .andThen((u) => (u.active ? ok(u) : err(ServiceError('FORBIDDEN', 'inactive'))))
  .orInspect((f) => f.capture())
runs when Ok runs when Err

One object. The whole story.

Build the failure once. toJSON() is what Sentry, OTel and your logger receive. location is filled in for you.

What you write

err(
  new Fault('stripe declined')
    .withTag('PAYMENT_FAILED')
    .withDetails('card_declined: insufficient_funds')
    .withMetadata({ intentId: 'pi_3Q…', amountCents: 4900 })
    .withCause(stripeError),
)

What your logger gets

{
  "name": "Error",
  "message": "stripe declined",
  "tag": "PAYMENT_FAILED",
  "details": "card_declined: insufficient_funds",
  "statusCode": 402,
  "location": "at chargeUser (src/billing.ts:41:12)",
  "metadata": {
    "intentId": "pi_3Q…",
    "amountCents": 4900
  },
  "cause": {
    "name": "StripeCardError",
    "message": "Your card has insufficient funds.",
    "stack": "StripeCardError: Your card has insufficient funds.\n    at ..."
  },
  "stack": "Error: stripe declined\n    at chargeUser (src/billing.ts:41:12)\n    at ..."
}
No unknown in catch
Every failure is a Fault. Narrow by tag, not by instanceof roulette, and the type system knows what can fail.
and* runs on Ok, or* on Err
andThen, andCheck, andInspect on the success path; orElse, orInspect on the failure path. One rule reads any chain.
One hook to observability
Set Fault.onCapture once, call .capture() anywhere. toJSON() is exactly what your logger sees.

Zero dependencies. Integrations on subpaths.

The core has no runtime dependencies. Each integration lives on its own import path and only asks for the peer it wraps.

ImportExportsPeer
@thourum/faultResult, ResultAsync, ok, err, Fault, ServiceError, retrynone
@thourum/fault/fetchsafeFetchnone
@thourum/fault/zodsafeZodParse, fromZodErrorzod
@thourum/fault/drizzlesafeDb, DatabaseErrordrizzle-orm, pg
@thourum/fault/pgparsePgError, isPostgresErrorpg
@thourum/fault/stdsafeJsonParse, safeJsonStringify, safeReadFile, safeWriteFile, safeEnvnone