NIST AU-3 requires that logged records contain sufficient information to establish what happened, when, who was involved, and what was affected. CWE-778 covers the failure mode directly: when errors are caught and logged with only err.message, the stack trace is discarded and the context of what triggered the error is lost. In practice this means that when a payment failure or auth error occurs at 3 AM, the engineer on call has a string like 'Unexpected token in JSON' and nothing else to work from — no file, no line, no user ID, no operation. The missing stack trace typically adds 20–60 minutes to incident resolution. OWASP A09 calls out insufficient error context as a core logging failure.
High because incomplete error logs remove the evidence trail needed to diagnose production incidents, directly extending mean time to resolution.
Pass the full error object as a property — not err.message as a string — so your logging library serializes the stack trace automatically.
// WRONG — stack trace is discarded
logger.error(err.message)
logger.error('Payment failed: ' + err.message)
// RIGHT — stack trace preserved, context included
logger.error({ err, userId, operationId: 'create-subscription' }, 'Subscription creation failed')
Pino, Winston, and Bunyan all serialize Error objects (including .stack) when they appear as a property of the first argument. Search for catch (err) blocks across lib/ and app/api/ and verify each one passes { err, ...context }, not just err.message.
ID: saas-logging.app-logging.error-logging-stack-context
Severity: high
What to look for: Enumerate all relevant files and Examine error handling code — try/catch blocks, global error handlers, and error boundaries. Check whether caught errors are logged with: (1) the full error object or stack trace, and (2) contextual information about what was happening (user ID, operation name, relevant parameters that aren't sensitive). Look for patterns like logger.error(err.message) (loses stack trace) vs logger.error({ err, userId }, 'Operation failed') (preserves stack and context).
Pass criteria: At least 1 implementation must be present. Caught errors in server-side code are logged with both the error object (including stack trace) and at least one piece of operational context (user ID, operation name, or request ID). The full error is passed to the logger, not just err.message.
Fail criteria: Errors are logged with only the message string (losing the stack trace), OR errors are caught and silently swallowed without any logging, OR error logging has no contextual fields — just the error with no information about what was happening.
Skip (N/A) when: No server-side code with error handling. Static-site only.
Detail on fail: Describe the pattern found. Example: "Error handling in src/app/api/billing/route.ts logs err.message only — stack trace not captured" or "Multiple catch blocks in lib/payments.ts swallow errors silently with no logging"
Remediation: A stack trace tells you where an error occurred; context tells you why. Without both, you're flying blind when an incident happens at 2 AM.
Most structured loggers pass the error object correctly when you include it in the log data:
// WRONG — loses stack trace
logger.error(err.message)
logger.error('Payment failed: ' + err.message)
// RIGHT — preserves stack and adds context
logger.error({ err, userId, operationId: 'create-subscription' }, 'Subscription creation failed')
Pino, Winston, and Bunyan all serialize error objects (including stack traces) when passed as a property of the first argument object. Consult your library's docs for the exact pattern.