Skip to content

Commit

Permalink
Preserve stacktrace when wrapping errors (#935)
Browse files Browse the repository at this point in the history
  • Loading branch information
szmarczak authored and sindresorhus committed Nov 23, 2019
1 parent b7a356a commit 8874a45
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 4 deletions.
18 changes: 16 additions & 2 deletions source/errors.ts
Expand Up @@ -7,7 +7,7 @@ export class GotError extends Error {
code?: string;
readonly options: NormalizedOptions;

constructor(message: string, error: (Error & {code?: string}) | {code?: string}, options: NormalizedOptions) {
constructor(message: string, error: Partial<Error & {code?: string}>, options: NormalizedOptions) {
super(message);
Error.captureStackTrace(this, this.constructor);
this.name = 'GotError';
Expand All @@ -19,6 +19,20 @@ export class GotError extends Error {
Object.defineProperty(this, 'options', {
value: options
});

// Recover the original stacktrace
if (!is.undefined(error.stack)) {
const indexOfMessage = this.stack.indexOf(this.message) + this.message.length;
const thisStackTrace = this.stack.slice(indexOfMessage).split('\n').reverse();
const errorStackTrace = error.stack.slice(error.stack.indexOf(error.message) + error.message.length).split('\n').reverse();

// Remove duplicated traces
while (errorStackTrace.length !== 0 && errorStackTrace[0] === thisStackTrace[0]) {
thisStackTrace.shift();
}

this.stack = `${this.stack.slice(0, indexOfMessage)}${thisStackTrace.reverse().join('\n')}${errorStackTrace.reverse().join('\n')}`;
}
}
}

Expand Down Expand Up @@ -96,7 +110,7 @@ export class TimeoutError extends GotError {
event: string;

constructor(error: TimedOutError, timings: Timings, options: NormalizedOptions) {
super(error.message, {code: 'ETIMEDOUT'}, options);
super(error.message, error, options);
this.name = 'TimeoutError';
this.event = error.event;
this.timings = timings;
Expand Down
11 changes: 9 additions & 2 deletions source/request-as-event-emitter.ts
Expand Up @@ -14,6 +14,7 @@ import {CacheError, MaxRedirectsError, RequestError, TimeoutError} from './error
import urlToOptions from './utils/url-to-options';
import {NormalizedOptions, Response, ResponseObject} from './utils/types';

const setImmediateAsync = () => new Promise(resolve => setImmediate(resolve));
const pipeline = promisify(stream.pipeline);

const redirectCodes: ReadonlySet<number> = new Set([300, 301, 302, 303, 304, 307, 308]);
Expand Down Expand Up @@ -283,7 +284,13 @@ export default (options: NormalizedOptions) => {
}
};

setImmediate(async () => {
(async () => {
// Promises are executed immediately.
// If there were no `setImmediate` here,
// `promise.json()` would have no effect
// as the request would be sent already.
await setImmediateAsync();

try {
for (const hook of options.hooks.beforeRequest) {
// eslint-disable-next-line no-await-in-loop
Expand All @@ -294,7 +301,7 @@ export default (options: NormalizedOptions) => {
} catch (error) {
emitError(error);
}
});
})();

return emitter;
};
Expand Down
14 changes: 14 additions & 0 deletions test/error.ts
Expand Up @@ -197,3 +197,17 @@ test('errors are thrown directly when options.stream is true', t => {
message: 'Parameter `hooks` must be an Object, not boolean'
});
});

test('the old stacktrace is recovered', async t => {
const error = await t.throwsAsync(got('https://example.com', {
request: () => {
throw new Error('foobar');
}
}));

t.true(error.stack.includes('at Object.request'));

// The first `at get` points to where the error was wrapped,
// the second `at get` points to the real cause.
t.not(error.stack.indexOf('at get'), error.stack.lastIndexOf('at get'));
});

0 comments on commit 8874a45

Please sign in to comment.