How to log custom error properties without blooming payloads
Extending native Error objects with contextual metadata often triggers payload bloat. This causes observability platforms to silently drop critical stack traces. This guide details how to safely attach custom properties while enforcing strict serialization boundaries. For foundational context on error propagation, refer to Core JavaScript Error Handling & Boundaries.
Key architectural constraints:
- Native
Errorserialization captures enumerable properties by default. - Direct object attachment triggers deep traversal and circular reference crashes.
- Use non-enumerable properties and explicit
toJSONoverrides. - Implement pre-flight payload size gating before network transmission.
Symptom Identification & Root Cause Analysis
Observability SDKs serialize Error instances via JSON.stringify. They transmit the result to ingestion endpoints. Attaching large state objects directly to error instances multiplies payload size exponentially. This frequently triggers 413 Request Entity Too Large responses.
Circular references cause serialization to fail entirely. The tracking service strips stack traces from the dashboard. Unhandled async failures often compound this issue when caught globally. The accumulation of request payloads in global catch handlers is a primary vector. See Handling Unhandled Promise Rejections in Modern JS for async interception patterns.
Minimal Reproduction of Payload Bloat
Direct property assignment makes attached objects enumerable. JSON.stringify traverses nested structures recursively. It applies no depth limits. Attaching a multi-megabyte buffer or deeply nested config object to a thrown error demonstrates the failure mode immediately.
const err = new Error('Payment failed');
err.userSession = massiveSessionObject; // 4MB+
err.circularRef = { self: err };
// SDK JSON.stringify(err) -> 4MB+ payload or crash
This pattern bypasses size constraints. The enumerable userSession forces recursive traversal. The circularRef triggers infinite recursion. The observability SDK either drops the event or throws a serialization error.
Production-Safe Extension Pattern
Implement a bounded Error subclass for structured error logging. Hide heavy context using Object.defineProperty with enumerable: false. Override toJSON() to explicitly whitelist diagnostic fields.
class BoundedError extends Error {
constructor(message, context) {
super(message);
this.name = 'BoundedError';
// Hide heavy context from default traversal
Object.defineProperty(this, '_context', {
value: context,
enumerable: false,
writable: false,
configurable: false
});
}
toJSON() {
// Explicitly whitelist lightweight diagnostic fields
return {
name: this.name,
message: this.message,
stack: this.stack,
context: {
userId: this._context?.userId,
endpoint: this._context?.endpoint
}
};
}
}
This preserves the standard Error contract. It guarantees a predictable JSON representation. The _context property remains accessible in memory. It is excluded from automatic serialization.
Pre-Transmission Payload Gating
Serialization safety must extend to the transport layer. Calculate the byte length of the payload before transmission. Implement a hard cutoff that truncates oversized custom properties. Fallback to a minimal signature when limits are breached.
function safeSerializeError(err, maxBytes = 65536) {
const payload = JSON.stringify(err);
if (new Blob([payload]).size > maxBytes) {
return JSON.stringify({
name: err.name,
message: err.message,
stack: err.stack,
truncated: true
});
}
return payload;
}
This utility enforces strict limits at the network boundary. It guarantees delivery of core stack traces. Integrate it into global error handlers. This prevents observability payload optimization failures.
Common Mistakes
Using JSON.stringify directly on Error instances without toJSON
Native Error properties (message, stack, name) are defined with enumerable: false. JSON.stringify only serializes enumerable own properties. Without an explicit override, it returns {}.
Attaching DOM nodes or File/Blob objects to error context
Observability SDKs cannot serialize DOM elements or binary objects. This results in [object Object] placeholders. It frequently causes serialization crashes or massive base64 expansion.
Ignoring async error context accumulation Global catch handlers often re-attach request/response payloads to every unhandled rejection. This causes linear payload growth during concurrent network failures.
Frequently Asked Questions
Why does JSON.stringify return an empty object for native Errors?
Native Error properties (message, stack, name) are defined with enumerable: false. JSON.stringify only serializes enumerable own properties unless a toJSON method is explicitly provided.
What is the recommended maximum payload size for error tracking SDKs?
Most platforms enforce a 64KB–128KB hard limit. Exceeding this triggers 413 errors or automatic truncation. Critical stack trace data is frequently dropped.
Can I safely attach circular references to custom errors?
No. Circular references cause JSON.stringify to throw a TypeError. Use WeakMap for tracking relationships. Explicitly serialize only primitive, acyclic subsets of state.