Integrations
Optional subpaths wrap throwers into Result / ResultAsync<..., Fault>. The root entry stays core-only.
Supported today
Peers are optional (peerDependenciesMeta). Install only the ones you import.
import { fromPromise, ServiceError } from '@thourum/fault'
const session = fromPromise(store.get(id), (e) =>
ServiceError('EXTERNAL_ERROR', String(e)),
)
import { safeFetchJSON } from '@thourum/fault/fetch'
const user = await safeFetchJSON<unknown>('/api/user').andThen((data) =>
safeZodParse(userSchema, data),
)
See safeFetch and safeFetchJSON.
import { safeZodParse } from '@thourum/fault/zod'
const parsed = safeZodParse(userSchema, body)
const parseUser = safeZodParse(userSchema)
See /zod.
import { safeDb } from '@thourum/fault/drizzle'
import { parsePgError } from '@thourum/fault/pg'
const row = safeDb(db.insert(posts).values(input).returning())
See /drizzle & /pg.
import { safeReadFile, safeJsonParse } from '@thourum/fault/std'
const config = await safeReadFile('./config.json').andThen(safeJsonParse)
See /std.
Wrap your own library
Built-in integrations do three things: wrap the thrower, normalise the rejection into a Fault, and export a safe* helper. /pg and /drizzle are this pattern — copy packages/fault/src/pg/index.ts and packages/fault/src/drizzle/index.ts.
- Wrap an async thrower with
fromPromise(promise, mapper) and a sync thrower with fromThrowable(fn, mapper).
- Write a mapper that turns
unknown into a tagged Fault.
- Export a helper that returns
Result / ResultAsync<..., Fault>.
Fault.from accepts Error | string. Coerce anything else first. Then tag, attach metadata the caller can act on, and keep the original value as the cause.
Give each failure mode the caller can branch on its own tag (CONNECTION_ERROR to retry, UNAUTHORIZED to refresh credentials). Unknown vendor codes fall back to a generic tag.
import {
Fault,
fromPromise,
fromThrowable,
type FaultTag,
type ResultAsync,
} from '@thourum/fault'
const REDIS_TAGS: Record<string, FaultTag> = {
ECONNREFUSED: 'CONNECTION_ERROR',
ENOTFOUND: 'CONNECTION_ERROR',
ETIMEDOUT: 'CONNECTION_ERROR',
NOAUTH: 'UNAUTHORIZED',
WRONGPASS: 'UNAUTHORIZED',
WRONGTYPE: 'EXTERNAL_ERROR',
}
function toRedisFault(e: unknown): Fault {
const code = (e as { code?: string }).code ?? 'UNKNOWN'
return Fault.from(e instanceof Error ? e : String(e))
.withTag(REDIS_TAGS[code] ?? 'UNKNOWN_ERROR')
.withMetadata({ code })
.withCause(e)
}
export function safeRedisGet(
redis: { get(key: string): Promise<string | null> },
key: string,
): ResultAsync<string | null, Fault> {
return fromPromise(redis.get(key), toRedisFault)
}
const safeYamlParse = fromThrowable(yaml.parse, (e) =>
Fault.from(e instanceof Error ? e : String(e))
.withTag('PARSE_ERROR')
.withCause(e),
)
parsePgError switches on SQLSTATE / Node codes the same way: known codes get a specific tag, class 08 and the rest collapse to CONNECTION_ERROR or DATABASE_ERROR.
Capture into Sentry, OpenTelemetry and loggers
Fault.onCapture is a single global hook. fault.capture() calls it and returns the same fault. toJSON() is the wire payload: name, message, details, tag, statusCode, location, metadata, cause, stack. Nested Fault causes are serialised recursively; other Error causes become { name, message, stack }.
import { Fault } from '@thourum/fault'
Fault.onCapture = (f) =>
Sentry.captureException(f, {
tags: { tag: f.tag },
extra: f.toJSON(),
})
Fault.onCapture = (f) => {
const payload = f.toJSON()
const metadata = (payload.metadata ?? {}) as Record<string, unknown>
span.recordException(f)
span.setStatus({ code: SpanStatusCode.ERROR })
span.setAttribute('fault.tag', String(payload.tag ?? ''))
span.setAttribute('fault.location', String(payload.location ?? ''))
for (const [key, value] of Object.entries(metadata)) {
span.setAttribute(`fault.metadata.${key}`, String(value))
}
}
Fault.onCapture = (f) => logger.error(f.toJSON(), f.message)
Rules:
- Set the hook once at startup.
- Capture once at the edge —
orInspect((f) => f.capture()) in the handler — not in every layer.
- The cause chain is included; walk it with
getCauseChain() when a backend wants the raw objects instead of the serialised cause field.
location is filled automatically from the first non-Fault stack frame at construction.
See Error tracing for the full flows.