Axios.request drops reconstructed caller stacks with fewer than three frames
Describe the issue
Axios.request decorates rejected native errors by capturing a second stack in its catch block and appending that stack to the original error. This restores the request call site for errors created asynchronously inside an adapter.
Since Axios 1.15.0, a reconstructed caller stack containing only one or two frames is not appended.
The behavior appears to be an unrelated side effect of commit 3631854, which replaced the previous regular-expression-based stack trimming with indexOf calls.
When the reconstructed stack has fewer than three frames, there is no second newline:
const stackWithoutTwoTopLines =
secondNewlineIndex === -1 ? '' : stack.slice(secondNewlineIndex + 1);
This makes the duplicate check equivalent to:
err.stack.endsWith('')
which is always true, so the reconstructed caller stack is skipped.
This is particularly visible for XHR errors created inside onloadend / settle. Their original stack describes the adapter callback, while a shallow V8 async chain may reconstruct only these two caller frames:
at Axios.request
at async requestFromNamedCaller
The regression is present in Axios 1.15.0 through 1.19.0.
Example code
Save as reproduction.mjs and run with Node:
import axios, { AxiosError } from 'axios';
const originalStackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = 2;
async function requestFromNamedCaller() {
await axios.request({
url: 'http://localhost/test',
adapter: () =>
new Promise((_, reject) => {
setTimeout(() => reject(new AxiosError('adapter failure')), 0);
}),
});
}
try {
await requestFromNamedCaller();
} catch (error) {
console.log(error.stack);
console.log(
'caller preserved:',
error.stack.includes('requestFromNamedCaller')
);
} finally {
Error.stackTraceLimit = originalStackTraceLimit;
}
Expected behavior
The reconstructed request stack should be appended, and the output should contain:
at Axios.request
at async requestFromNamedCaller
caller preserved: true
This matches the behavior before Axios 1.15.0.
Actual behavior
With Axios 1.19.0, the reconstructed stack is skipped:
caller preserved: false
The behavior differs depending on the reconstructed stack depth:
| Reconstructed caller stack | Axios ≤1.14 duplicate suffix | Axios ≥1.15 duplicate suffix |
|---|---|---|
| 1–2 frames | Entire reconstructed stack | Empty string |
| 3+ frames | Stack after the first two frames | Stack after the first two frames |
A possible fix is to preserve the pre-1.15 fallback:
const stackWithoutTwoTopLines =
- secondNewlineIndex === -1 ? '' : stack.slice(secondNewlineIndex + 1);
+ secondNewlineIndex === -1 ? stack : stack.slice(secondNewlineIndex + 1);
The existing duplicate detection for stacks containing three or more frames remains unchanged.
Environment
- Axios version: 1.19.0
- Adapter: Custom asynchronous adapter for the minimal reproduction; originally observed with XHR
- Runtime: Node.js 24.17.0; also observed in V8/Chrome
- OS: macOS for the minimal reproduction
- Additional context: The issue is most visible with shallow async call chains whose reconstructed caller stack contains only two frames.
:surfer:
Source: axios/axios