Concepts

Errors

Error-first calls, cause chains, own properties, and how each formatter renders them.

The error goes first#

try {
  await charge(order);
} catch (err) {
  log.error(err, 'Charge for {OrderId} failed', order.id);
}

The first argument of any level method may be an Error. It is kept on the event as event.error — apart from the properties, as Serilog keeps Exception — so every formatter can render it in full and every sink can inspect it.

log.error(err) with no template logs the error's message as the message (braces escaped). log.warn(err, 'Retrying') logs a warning that carries an error; the level is yours to choose.

If what you caught is not an Error — a string, a rejected promise's plain object — wrap it: new Error(String(thing), { cause: thing }).

What is captured#

event.errorProperties is the error as data, computed once:

{
  "$type": "Error",
  "name": "PaymentError",
  "message": "card declined",
  "stack": "PaymentError: card declined\n    at charge (…)",
  "code": "card_declined",
  "status": 402,
  "cause": { "$type": "Error", "name": "GatewayError", "message": "402", "stack": "…" }
}
  • name, message, stack
  • every own enumerable property — the code on a Node error, the status on an HTTP one, whatever your class PaymentError extends Error assigns in its constructor
  • cause, captured recursively (an Error cause is itself a structure; anything else is captured by the normal rules)
  • errors of an AggregateError

Redaction applies inside errors too — redact: ['password'] masks a password property a validation library attached. Depth and string limits apply.

How each output renders it#

Pretty console — the stack under the line, indented, each cause under it:

08:12:03.140 ERR [shop.payments] Charge for 1042 failed
    PaymentError: card declined
        at charge (/app/payments.ts:42:11)
      caused by: GatewayError: 402
          at post (/app/gateway.ts:17:9)

JSON — the structure above under error.

CLEF — the same text as the pretty block, in @x, which is what Seq shows as the exception.

Output templates — {Exception} renders the text block; the default template ends with {NewLine}{Exception} so it goes on the lines after the message.

OTLP — exception.type, exception.message, exception.stacktrace attributes, per the semantic conventions.

Errors as properties#

An Error passed to a hole (not first) is captured as the same structure, under the hole's name, and renders as Name: message inside the message — the stack is not spliced into a sentence:

log.warn('Fallback used after {@Primary}', primaryError);
// Fallback used after GatewayError: 402     … "Primary":{"$type":"Error","name":"GatewayError",…}

Crashes#

closeOnExit(log) from @unhingged/logit/node logs an uncaught exception or an unhandled rejection at fatal — Uncaught exception — the process will exit — flushes every sink, then exits with code 1. Without it Node prints the stack and exits before an HTTP sink has sent anything. See Production.

Next.js#

createOnRequestError() logs every error Next catches, with the route, the router kind and the request. React may wrap an error thrown during server rendering; the digest that identifies the original is logged as Digest. See Next.js.

Not yet#

Per-library destructurers — the useful fields of a Prisma, Axios or fetch error, without the noise — are on the roadmap. Today a byTransforming policy does it for the types you care about:

byTransforming(AxiosError, (e) => ({ message: e.message, status: e.response?.status, url: e.config?.url }))

A policy applies to errors captured as properties; the first-argument error is always captured in full.