Stack Traces for Promises in Node.js

Emanuel Jöbstl
Frontend Weekly
Published in
2 min readJan 31, 2018

--

A stack trace is one of the most fundamental tools when debugging code, since it will reliably tell us where an error happened. The concept of stack traces originates from synchronous code.

Error: Oh no a database error!
at getStuffFromDatabase (index.js:2:4)
at handleSomeBusinessLogic (index.js:22:2)
at runApi (index.js:34:3)

Using asynchronous code makes tracing somewhat difficult, since the corresponding calls are now indirect. If an uncaught Error is thrown inside a promise, we would see something like this:

Error: Oh no a database error!
at Connection.parseMessage (node_modules/db-lib/con.js:370:19)
at Socket.<anonymous> (node_modules/db-lib/con.js:113:22)
at addChunk (_stream_readable.js:265:12)
at readableAddChunk (_stream_readable.js:252:11)
at Socket.Readable.push (_stream_readable.js:209:10)
at TCP.onread (net.js:608:20)

The stack trace now points to some code inside our database library, which happens to handle input asynchronously. Most notable, all context of the caller is lost.

This is especially bad with async/await . Code semantics with async/await are beautiful, but stack traces are useless, since promises are used behind the scenes. That does not only make debugging harder, it is also virtually impossible to do test-driven development with this shortcoming.

Technically, it would be possible to trace promise invocations, but that is not yet implemented in node’s native promise implementation.

Luckily, there are 3rd party promise libraries, like bluebird, which implement tracing across several promise invocations.

To enable that feature, use bluebird as your promise implementation and set the environment variable BLUEBIRD_LONG_STACK_TRACESto 1 . The result is not exactly beautiful, but at least it pinpoints all callers, which I highlighted in this example:

Error: Oh no a database error!
at Connection.parseMessage (node_modules/db-lib/con.js:370:19)
at Socket.<anonymous> (node_modules/db-lib/con.js:113:22)
at addChunk (_stream_readable.js:265:12)
at readableAddChunk (_stream_readable.js:252:11)
at Socket.Readable.push (_stream_readable.js:209:10)
at TCP.onread (net.js:608:20)
From previous event:
at Database.$query (node_modules/db-lib/query.js:126:12)
at Database.<anonymous> (node_modules/db-lib/query.js:241:23)
at config.$npm.connect.db (node_modules/db-lib/db.js:307:42)
From previous event:
at step (index.js:2:5)
at getStuffFromDatabase (index.js:2:4)
From previous event:
at __awaiter (lib/sql/index.js:3:12)
at Object.sql [as default] (index.js:36:12)
at Object.<anonymous> (index.js:84:31)
at Generator.next (<anonymous>)
at fulfilled (index.js:6:58)
From previous event:
at step (index.js:8:124)
at handleSomeBusinessLogic (index.js:22:2)
at runApi (index.js:34:3)

You can use the following snippet to replace Node.js’ promise implementation by bluebird:

global.Promise=require(“bluebird”);

This also works for ts-node , which is very convenient when running Mocha tests. In this case, you can simply require the above snippet. If you use babel, you might want to use transform-async-to-bluebird instead.

Happy testing!

--

--

Emanuel Jöbstl
Frontend Weekly

CTO of Authory, versed in software engineering, fascinated by neural networks