Capture Javascript Async Stack Traces
Pavel Evstigneev
Posted on June 1, 2019
Javascript stacktrace do not include async calls, that sucks.
Example:
var sayHi = () => {
setTimeout(() => {
console.log('Hi there');
thisLineBreaks();
}, 1000);
};
sayHi();
Output:
$ node try_async.js
Hi there
/my_code/try_async.js:4
thisLineBreaks();
^
ReferenceError: thisLineBreaks is not defined
at Timeout.setTimeout [as _onTimeout] (/my_code/try_async.js:4:5)
at listOnTimeout (timers.js:324:15)
at processTimers (timers.js:268:5)
We can only see error details inside sayHi
function and we don't know where it was called.
Solution
To solve this, we can capture stack trace before async part and append it to actual error.
var sayHi = () => {
var startStack = new Error().stack;
setTimeout(() => {
try {
console.log('Hi there');
thisLineBreaks();
} catch (error) {
error.stack = error.stack + "\n" +
startStack.substring(startStack.indexOf("\n") + 1);
throw error;
}
}, 1000);
};
sayHi();
And now we getting it!
node try_async.js
Hi there
/my_code/try_async.js:9
throw error;
^
ReferenceError: thisLineBreaks is not defined
at Timeout._onTimeout (/my_code/try_async.js:6:7)
at listOnTimeout (internal/timers.js:531:17)
at processTimers (internal/timers.js:475:7)
at sayHi (/my_code/try_async.js:2:20)
at Object.<anonymous> (/my_code/try_async.js:14:1) <<<< HERE
at Module._compile (internal/modules/cjs/loader.js:774:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:785:10)
at Module.load (internal/modules/cjs/loader.js:641:32)
at Function.Module._load (internal/modules/cjs/loader.js:556:12)
at Function.Module.runMain (internal/modules/cjs/loader.js:837:10)
at internal/main/run_main_module.js:17:11
It show where my function was called — /my_code/try_async.js:14
Promises
Now days most of libraries uses promises and we can easily apply it for every function that returns promise object:
var trackPromise = async (func) => {
var startStack = new Error().stack;
try {
return await func();
} catch (error) {
error.stack = error.stack + "\n" +
startStack.substring(startStack.indexOf("\n") + 1);
throw error;
}
}
// will make complete stack traces
var rows = trackPromise(() => {
return db.query('select * from something');
});
Or modify async functions:
// patch method in object to keep stack traces
var addStackTracker = (proto, methodName) => {
var originalMethod = proto[methodName];
proto[methodName] = function(...args) {
var startStack = new Error().stack;
return new Promise((resolve, reject) => {
originalMethod.apply(this, args).then(res => { resolve(res); })
.catch(error => {
error.stack = error.stack + "\n" + startStack.substring(startStack.indexOf("\n") + 1);
reject(error);
});
});
}
}
addStackTracker(DatabaseClient.prototype, 'query')
var db = new DatabaseClient()
db.query('...') // will have full stack
now my dev life became easier.
Note: if your function call multiple async functions inside, than this approach will keep only stack before it enter function and stack for last async call
Posted on June 1, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.