DEV Community

Pavel Evstigneev
Pavel Evstigneev

Posted on

Capture Javascript Async Stack Traces

Javascript stacktrace do not include async calls, that sucks.

Example:

var sayHi = () => {
  setTimeout(() => {
    console.log('Hi there');
    thisLineBreaks();
  }, 1000);
};

sayHi();
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)