Fault

Fault is an Error with a tag, developer details, metadata, causes, HTTP status mapping, and an optional capture hook. Every with* builder returns a new Fault without changing the original.

new Fault

new Fault(initial: Error | string | Fault): Fault

const fault = new Fault(new Error('stripe declined'))

A new fault keeps the input message and forwards only the input error's existing cause; use .withCause(err) to attach the original error itself.

Fault.from

Fault.from(initial: Error | string): Fault

const fault = Fault.from(new Error('connection reset'))
  .withTag('CONNECTION_ERROR')

Use Fault.from when converting an error or message without calling the constructor directly.

withTag

withTag(tag: FaultTag): this

const fault = new Fault('user missing')
  .withTag('NOT_FOUND')

withTag returns a new fault with the replacement classification.

withDetails

withDetails(details: string): this

const fault = new Fault('payment failed')
  .withDetails('card_declined: insufficient_funds')

withDetails stores developer-facing context without changing the user-facing message.

withDescription

withDescription(details: string, message?: string): this

const fault = new Fault('database timeout').withDescription(
  'PostgreSQL connection timed out after 5 seconds',
  'Service temporarily unavailable',
)

withDescription stores developer details and replaces the message only when the optional message is provided.

withMetadata

withMetadata(key: string, value: unknown): this / withMetadata(data: Record<string, unknown>): this

const fault = new Fault('stripe declined')
  .withMetadata('intentId', 'pi_3Q…')
  .withMetadata({ amountCents: 4900, currency: 'eur' })

Both overloads return a new fault with merged metadata.

withContext

withContext(data: Record<string, unknown>): this

const fault = new Fault('database unavailable').withContext({
  attemptCount: 3,
  operation: 'CREATE_PAYMENT',
})

withContext is the object-form metadata merger used by withMetadata.

withCause

withCause(cause: unknown): this

const fault = new Fault('payment failed')
  .withCause(new Error('socket closed'))

withCause returns a new fault with the thrown value as its standard Error.cause.

tag

get tag(): FaultTag | undefined

const tag = new Fault('missing').withTag('NOT_FOUND').tag

tag returns the current classification; every newly constructed fault starts as UNKNOWN_ERROR.

details

get details(): string | undefined

const details = new Fault('invalid input')
  .withDetails('email is not RFC 5322 compliant')
  .details

details returns the developer-facing description when one has been set.

location

get location(): string | undefined

const location = new Fault('failed').location

location is the stack location captured when the fault was constructed, when available.

metadata

get metadata(): Record<string, unknown>

const metadata = new Fault('failed')
  .withMetadata({ requestId: 'req_123' })
  .metadata

metadata returns a shallow copy so changing the returned object does not change the fault.

statusCode

get statusCode(): number

const status = new Fault('missing')
  .withTag('NOT_FOUND')
  .statusCode
TagStatus
VALIDATION_ERROR, BAD_REQUEST, FOREIGN_KEY_ERROR400
AUTHENTICATION_ERROR, UNAUTHORIZED401
PAYMENT_FAILED402
AUTHORIZATION_ERROR, FORBIDDEN403
NOT_FOUND404
CONFLICT, UNIQUE_CONSTRAINT_ERROR409
RATE_LIMITED429
ABORTED (client closed request)499
NETWORK_ERROR, CONNECTION_ERROR503
TIMEOUT_ERROR504
EXTERNAL_ERROR502
PARSE_ERROR, HTTP_ERRORmetadata.httpStatus or 500
DATABASE_ERROR, INTERNAL_ERROR, UNKNOWN_ERROR, and every other tag500

statusCode follows this mapping and defaults unlisted or custom tags to 500.

toJSON

toJSON(): Record<string, unknown>

{
  "name": "Error",
  "message": "stripe declined",
  "details": "card_declined: insufficient_funds",
  "tag": "PAYMENT_FAILED",
  "statusCode": 402,
  "location": "at chargeUser (src/billing.ts:41:12)",
  "metadata": {
    "intentId": "pi_3Q…",
    "amountCents": 4900
  },
  "cause": {
    "name": "Error",
    "message": "boom",
    "stack": "Error: boom\n    at ..."
  },
  "stack": "Error: stripe declined\n    at chargeUser (src/billing.ts:41:12)\n    at ..."
}

toJSON emits structured fields for logging, serializes Error causes, and recursively serializes a cause that is another Fault; redact sensitive details, metadata, and non-Error causes before logging.

getCauseChain

getCauseChain(): Array<Error | unknown>

const root = new Error('socket closed')
const chain = new Fault('payment failed')
  .withCause(root)
  .getCauseChain()

getCauseChain returns this fault followed by each Error cause and any final truthy non-Error cause.

capture / Fault.onCapture

capture(): this / Fault.onCapture: ((fault: Fault) => void) | undefined

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

new Fault('payment failed').capture()

capture invokes the current hook when configured and always returns the same fault unchanged.

ServiceError

ServiceError(tag: FaultTag, message: string, description?: string): Fault

const fault = ServiceError(
  'NOT_FOUND',
  'User not found',
  'No user matched usr_123',
)

ServiceError creates a tagged fault whose details are the description or, when omitted, the message.

FaultTag

FaultTag includes the predefined groups from the source and accepts custom string tags.

type FaultTag =
  // Auth
  | 'AUTHENTICATION_ERROR'
  | 'AUTHORIZATION_ERROR'
  // Resource
  | 'NOT_FOUND'
  | 'DATABASE_ERROR'
  // Network/API
  | 'NETWORK_ERROR'
  | 'TIMEOUT_ERROR'
  | 'ABORTED'
  | 'PARSE_ERROR'
  | 'HTTP_ERROR'
  // Data
  | 'VALIDATION_ERROR'
  | 'BAD_REQUEST'
  | 'UNAUTHORIZED'
  | 'FORBIDDEN'
  | 'CONFLICT'
  | 'RATE_LIMITED'
  | 'PAYMENT_FAILED'
  | 'EXTERNAL_ERROR'
  | 'CONFIGURATION_ERROR'
  | 'CONNECTION_ERROR'
  | 'UNIQUE_CONSTRAINT_ERROR'
  | 'FOREIGN_KEY_ERROR'
  | 'TRANSACTION_ROLLBACK_ERROR'
  // Catch-all
  | 'INTERNAL_ERROR'
  | 'UNKNOWN_ERROR'
  // Custom
  | (string & {})

Use a predefined tag when it fits; string & {} keeps autocomplete while allowing domain-specific tags.