Asynchronous error handling in Node.js

Due to its asynchronous nature, the BugSnag SDK has some special considerations for Node.js apps to ensure errors are reported in the correct context. This ensures, for example, that only data relevant to the current request of a webserver is captured in an error report.

Handling asynchronous errors in Node.js can also involve a lot of boilerplate. Our library provides some utilities to help reduce the amount of code you have to write in order to achieve decent error handling.

This documentation is for version 7+ of the BugSnag JavaScript notifier. If you are using older versions, we recommend upgrading to the latest release using our Upgrade guide. Documentation for the previous release can be found on our legacy pages.

Accessing a request-scoped Bugsnag client

The BugSnag SDK uses Node’s AsyncLocalStorage mechanism to store and retrieve a Bugsnag client that is scoped to the request being processed. This prevents metadata and breadcrumbs added in one request being captured in events reported by other requests. This allows you to call Bugsnag.addMetadata, Bugsnag.leaveBreadcrumb and Bugsnag.addFeatureFlag and it only affects events that occur within the same request.

Alternatively, the BugSnag middleware attaches a request-scoped client to the req object in your app, allowing you to call BugSnag SDK methods using req.bugsnag.

Prior to v8 of the BugSnag SDK, req.bugsnag is the only way to get a request-scoped client.

Context loss in Express servers

For most apps this context storage is seamless. However, there are rare situations on Express servers when this contextual storage can get lost, causing the data stored to become server-scoped and so affect all threads that are being executed.

This is usually because a middleware has called next() from within an event handling function attached to the req object. For example:

app.use(function (req, res, next) {
  // do some body parsing for example
  req.on('end', function () {
    next()
  })
})

In this case, the event handler, and therefore the next() call is executed in the server-context and the request-scoped client stored in an AsyncLocalStorage instance cannot be retrieved. That request context will be lost for all subsequent middleware.

In such situations, we make the following suggestions:

Update body-parsing middlewares

This issue was a common problem in body-parsing middlewares and has been fixed in more recent versions of these libraries so updating such dependencies may be a solution to any context-loss you are experiencing.

Re-attach the context

You can use the runInContext middleware to re-attach the context right after the middleware that is causing context-loss. For example:

const middleware = Bugsnag.getPlugin('express')

app.use(middleware.requestHandler)

// problematic middleware
app.use(function (req, res, next) {
  // do some body parsing for example
  req.on('end', function () {
    next()
  })
})

// context is lost due to the above middleware so we re-attach it here

app.use(middleware.runInContext)

app.use(function (req, res, next) {
  Bugsnag.notify('request-context is back')
  next()
})

Explicitly bind the event handler

You can fix the middleware itself by binding the event handler to the current AsyncResource context. For example:

app.use(function (req, res, next) {
  // do some body parsing for example
  req.on('end', AsyncResource.bind(function () {
    next()
  }))
})

Use the request-scoped client

If you cannot fix the context loss, you can always use req.bugsnag. instead of Bugsnag. to ensure the request context is included in error reports. There is never an issue with context-loss when using req.bugsnag..

app.use(function (req, res, next) {
  // do some body parsing for example
  req.on('end', function () {
    next()
  })
})

// context is lost due to the above middleware so whilst `Bugsnag.notify`
// will not contain request-context, `req.bugsnag.notify` will

app.use(function (req, res, next) {
  req.bugsnag.notify('calls made using req.bugsnag always have request-context')
  next()
})

intercept(cb)

Many operations in Node.js are asynchronous.

When you write asynchronous code with Node-style error first callbacks, or using promises, you must handle every error otherwise they will be swallowed and you will never know about them.

Handling every error can be repetitive, so BugSnag provides an intercept() function which will handle the error for you, and call your callback only if the operation was successful.

// intercept() is a plugin which is built in by default
// The way you access plugins is to retrieve them by name from the client…

var intercept = Bugsnag.getPlugin('intercept')

API

intercept(optionalCb)

Returns a function which you can pass as a Node-style error first callback, or as a Promise catch clause. If you supply the optional cb, it will be called only if there was no error parameter with the error parameter removed from the arguments.

Examples

Without intercept()

fs.readFile('does_not_exist.txt', function (err, data) {
  if (err) {
    return Bugsnag.notify(err)
  }
})

With intercept()

fs.readFile('does_not_exist.txt', intercept())

Providing an optional callback

fs.readFile('does_not_exist.txt', intercept(function (data) {
  // notice there is no `err` parameter in this callback. it
  // is only ever called when no err is present.
  console.log('success!')
}))

Using promises

var readFile = promisify(fs.readFile)
readFile
  .then(function (data) {
    console.log('success')
  })
  .catch(intercept()) // << doesn't need a callback because that goes in .then()

contextualize(cb)

When asynchronous errors happen in Node.js, it’s often hard to track them back to where they originated. This is because the stacktraces are not maintained in asynchronous execution.

To provide context around errors from a particular part of your application, you can use contextualize(cb) to attach info that might be useful later on. This info will be attached to any errors detected by BugSnag which originated there.

var contextualize = Bugsnag.getPlugin('contextualize')

Unhandled errors that occur within a contextualize context respect the autoDetectErrors and enabledErrorTypes configuration options.

Prior to v8 of the BugSnag SDK unhandled errors would have been caught regardless of the configuration.

API

contextualize(cb, onError)

Executes cb(). Any error that can be traced back to cb() will trigger the onError callback, the same as the onError parameter of notify(), which provides access to the event data.

Example

contextualize(
  function () {
    fs.createReadStream('does not exist')
  },
  function (event) {
    event.addMetadata('component', 'name', 'file reader')
    event.context = 'cmp/file_reader'
  }
)