Reputation: 2042
I'm trying to understand how can I get a full stack trace from a promise rejection caused by a setTimeout
I'm running the following example:
'use strict';
function main() {
f1().catch(e => {
console.error('got error with trace:');
console.error(e);
});
f2().catch(e => {
console.error('got error with trace:');
console.error(e);
});
}
async function f1() {
return new Promise((resolve, reject) => {
reject(new Error('Error in normal flow'));
});
}
async function f2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('Error in timeout'));
}, 0);
});
}
main();
And I'm getting this output:
got error with trace:
Error: Error in normal flow
at Promise (/Users/me/project/example.js:25:12)
at Promise (<anonymous>)
at f2 (/Users/me/project/example.js:24:10)
at main (/Users/me/project/example.js:9:3)
at Object.<anonymous> (/Users/me/project/example.js:29:1)
at Module._compile (module.js:569:30)
at Object.Module._extensions..js (module.js:580:10)
at Module.load (module.js:503:32)
at tryModuleLoad (module.js:466:12)
at Function.Module._load (module.js:458:3)
got error with trace:
Error: Error in timeout
at Timeout.setTimeout [as _onTimeout] (/Users/me/project/example.js:18:14)
at ontimeout (timers.js:488:11)
at tryOnTimeout (timers.js:323:5)
at Timer.listOnTimeout (timers.js:283:5)
How can I make make the stack trace of the promise that is initiated with setTimeout be more verbose like the promise without setTimeout
?
When this happen to me in real production code, I can't know exactly where the error was initiated from. Which makes it very difficult to debug.
Upvotes: 2
Views: 1814
Reputation: 1771
I've solved it like this: https://github.com/NaturalCycles/js-lib/blob/master/src/promise/pTimeout.ts
pTimeout
function code:
export async function pTimeout<T>(promise: Promise<T>, opt: PTimeoutOptions): Promise<T> {
const { timeout, name, onTimeout, keepStackTrace = true } = opt
const fakeError = keepStackTrace ? new Error('TimeoutError') : undefined
// eslint-disable-next-line no-async-promise-executor
return await new Promise(async (resolve, reject) => {
// Prepare the timeout timer
const timer = setTimeout(() => {
const err = new TimeoutError(`"${name || 'pTimeout function'}" timed out after ${timeout} ms`)
if (fakeError) err.stack = fakeError.stack // keep original stack
reject(err)
}, timeout)
// Execute the Function
try {
resolve(await promise)
} catch (err) {
reject(err)
} finally {
clearTimeout(timer)
}
})
}
Upvotes: 0
Reputation: 19849
There are some decent answers in here, but on thing that hasn't been considered is that generating a stack trace is expensive and you don't want to do it all the time. V8 has a fancy JIT compiler that can run things out of order and has to be unwound to get a proper stack trace. Thus we should only generate the stack trace for the "potential error" if and when we need it.
async function f2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
// Stack trace is unhelpful here.
reject(new Error("Error in timeout"))
}, 0)
})
}
async function f3() {
// This stack trace is useful here, but generating a stack trace is expensive
// because V8 has to unravel all of its fancy JIT stuff.
const potentialError = new Error("Error in timeout")
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(potentialError)
}, 0)
})
}
async function f4() {
try {
const result = await new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("Error in timeout"))
}, 0)
})
return result
} catch (error) {
// Override the stack trace only when you need it.
error.stack = new Error().stack
throw error
}
}
Upvotes: 1
Reputation: 664630
I'd try to write it like this:
async function f() {
await new Promise((resolve, reject) => {
setTimeout(resolve, 0);
});
throw new Error('Error after timeout');
}
Try to avoid doing anything in non-promise callbacks.
Upvotes: 1
Reputation: 6769
I've done something like the following before:
async function f2() {
return new Promise((resolve, reject) => {
const potentialError = new Error('Error in timeout');
setTimeout(() => {
reject(potentialError);
}, 0);
});
}
That'll result in an error just like the one outside setTimeout
. Wherever you create your error, that'll dictate your stack trace.
One use case for this for me has been a test that times out because a promise never resolves (in my case, with puppeteer and mocha/jasmine). Because the timeout had no meaningful stack trace, I wrote a wrapper around the promise that includes a setTimeout
, much like this example.
Upvotes: 1