Published: August 15, 2026 · by Srinu Desetti · axios/axios#10913 (opens in a new tab)
How I Fixed Error Serialization Crashes in Axios
Axios is used by millions of developers worldwide. My contribution fixed a bug in its fetch adapter where logging a network error could crash the application with TypeError: Converting circular structure to JSON.
What Was the Issue?
The bug was not that Axios created a circular reference.
The circular reference already existed inside some Node.js fetch network errors. Axios accidentally exposed that circular object by making the cause property enumerable.
Where Did the Circular Reference Come From?
When a network request fails (for example ECONNREFUSED or a DNS failure), Node.js creates an internal error object. That error may reference internal objects such as the socket, request, stream, and parser — and those internal objects may reference each other:
Socket
│
▼
Request
│
▼
Socket ← back to where we started: a cycleThe Old Axios Code
Object.assign() creates enumerable properties. So the AxiosError became:
AxiosError
├── message
├── code
└── cause (enumerable ← the problem)What does enumerable mean?
Enumerable means JavaScript includes the property when iterating:
Object.keys(error)
for (const k in error) {}
JSON.stringify(error)
{ ...error }All of these walk enumerable properties.
Why Did Logging Crash?
Loggers like Pino and Winston serialize errors by walking their enumerable properties:
AxiosError → cause → originalError → socket → request → socket → …The logger keeps traversing the circular references and eventually throws:
TypeError: Converting circular structure to JSONThe logger crashes — not because Axios created a circular reference, but because it was allowed to walk into one.
The Fix
Replace Object.assign with Object.defineProperty, making cause non-enumerable — the same behavior as JavaScript's native Error:
Now cause still exists, but is hidden from iteration:
AxiosError
├── message
├── code
└── cause (non-enumerable ← hidden from loggers)What Changed for Users
Direct access still works — debugging is fully preserved:
error.cause // ✅ still works
error.cause.code // ✅ still worksBut automatic traversal skips it:
Object.keys(error) // no cause
JSON.stringify(error) // no crashThe logger never reaches the circular Node.js internals.
Important clarification — the fix did not remove the circular reference. It still exists inside the original Node.js error. The fix only prevents serializers from automatically traversing into it, matching native Error behavior.
An Analogy
Think of a house with a hall, a kitchen, and a secret room containing a dangerous machine. A cleaning robot enters every room and gets stuck in the secret room. The fix doesn't remove the machine — it hides the door from the robot. The robot cleans the visible rooms and never gets stuck.
Impact
- Applications using Axios's fetch adapter no longer crash while logging network errors — a failure path that previously turned "the network blipped" into "the process threw during error handling".
- Structured loggers (Pino, Winston) serialize AxiosErrors safely out of the box.
- Debugging is unchanged:
error.causeremains fully accessible, now consistent with how native JavaScriptErrortreatscause.
What I Contributed
- Identified the root cause: an enumerable
causeexposing Node's circular error internals. - Implemented the fix with
Object.defineProperty. - Wrote comprehensive test cases.
- Collaborated with the Axios maintainers through review until the PR was merged.
View the pull request → axios/axios#10913 (opens in a new tab)
← All contributions · Next: React Redux error reporting →
Frequently Asked Questions
Did Axios create the circular reference?
No. The circular reference already existed inside Node.js fetch network errors, which internally reference the socket, request, and parser objects that reference each other. Axios only exposed it by attaching the error as an enumerable cause property.
Did the fix remove the circular reference?
No. The circular structure still exists inside the original Node.js error. The fix makes the cause property non-enumerable, so serializers and loggers skip it during iteration while direct access via error.cause keeps working.
Why did loggers like Pino and Winston crash?
They serialize errors by walking enumerable properties. With cause enumerable, they traversed into the circular Node.js internals and threw TypeError: Converting circular structure to JSON.
Why use Object.defineProperty instead of Object.assign?
Object.assign always creates enumerable properties. Object.defineProperty gives explicit control — the fix sets enumerable: false while keeping the property writable and configurable, matching how native JavaScript Error handles cause.