Skip to content

Error handling

Sandor edited this page Dec 8, 2018 · 3 revisions

Currently, server will crash on any developer error. Errors created with boom.badImplementation(message, data) set isDeveloperError: true. In all other cases, whenever stack trace is available or statusCode is missing, an error is automatically marked as developer error. BOOM HTTP-friendly error objects is used to decorate errors.

Errors can be handled in a centralized way using Express error handling middleware (must be before the final error handling middleware):

app.use((err, req, res, next) => {
    if (err.statusCode === 400) {
        // do something
    } else {
        next(err);
    }
});

IMPORTANT! Remember that async errors must be passed to Express.js using the 'next(err)' function otherwise they will not be handled.

Documentation on Error Handling in Express.js

Various errors and ways they are handled:

1. A simple error thrown synchronously. Express.js will catch this on its own.

throw new Error('Synchronous Error. Server will crash!');

2. Asynchronous error. Express.js will catch it only if it is passed to next(err)

 app.get('/', function (req, res, next) {
     fs.readFile('/file-does-not-exist', function (err, data) {
         if (err) {
             next(err); // Pass errors to Express.
         }
         else {
             res.send(data);
         }
     });
 });

 app.get('/', function (req, res, next) {
     setTimeout(function () {
         try {
             throw new Error('Asynchronous Error. Server will crash!');
         }
         catch (err) {
             next(err);
         }
     }, 100);
 });

 //Use Promise to avoid `try ... catch`
 app.get('/', function (req, res, next) {
     Promise.resolve().then(function () {
         throw new Error('Asynchronous Error. Server will crash!');
     }).catch(next); // Errors will be passed to Express.
 });

3. Unhandled promise rejection

Whenever a Promise is rejected and no error handler is attached to the promise, 'unhandledRejection' event is emitted. This causes server hang for the client (504 Gateway Time-out), but server handles requests from other clients in the meantime. Server does NOT crash.

// 'response' variable has not been defined
// catching errors in error callback does not work here as promise resolved successfully
// and we now deal with an error in success callback
 app.get('/', function (req, res, next) {
     Promise.resolve().then(() => {
         res.send(`This is the response ${response}\n`);
     });
     // .catch(next); // Errors would be passed to Express if `catch()` was used
 };

 // error in error callback of a rejected promise
 app.get('/', function (req, res, next) {
     Promise.reject(
         new Error(
             'Unhandled promise rejection error with an error in error callback of a rejected promise',
         ),
     ).then(
         () => {
             // 'will not get here'
         },
         (err) => {
             next(error); // `error` is not defined
         },
     );
     // .catch(next); // Errors would be passed to Express if either `catch()` or error callback was used
     };

4. Error handling with async ... await

Passing an error to Express this way: return next(boom.badImplementation('Missing id')) from within an async function ensures, it is handled as expected. However throwing an error throw boom.badImplementation('Missing id') will raise an Unhandled Rejection at Promise warning. Therefore, errors should be thrown inside try...catch block to be handled by catch().

If a promise resolves successfully, then await promise returns a result. However, in case of a rejection, it throws the error. Such errors can be caught using try..catch

try {
    const response = await promise('url-does-not-exists/data.json');
    res.json(response);
} catch (err) {
    next(err); // Pass error to Express
}

NOTE! Without try..catch there will be a process event uncaughtException emitted with a warning Unhandled Rejection at Promise

Another way to handle possible errors with async ... await is to use a wrapper to ensure errors are propagated. Here is the wrapper implementation.

 // wrapper for our async route handlers
 const asyncMiddleware = fn => (req, res, next) => {
     // eslint-disable-next-line consistent-return
     Promise.resolve(fn(req, res, next)).catch((err) => {
         if (!err.isBoom) return next(boom.badImplementation(err));

         next(err);
     });
 };

And it used like this:

router.get('/async-await', asyncMiddleware((req, res, next) => {});

Rejected promises will be handled correctly. If you need to throw an error, just throw instead of calling next()

 module.exports.asyncAwaitError = async (req, res) => {
     const { id } = req.params;
     if (!id) throw boom.badImplementation('Missing id');

     const response = await readFilePromise('url-does-not-exists/data.json');
     res.json(response);
 };

5. Error handling and streams Errors emitted by streams should be handled by error event listeners stream.on('error', (err) => {}. This gives developers an opportunity to decide how such error should be treated. It is important to note, that next() should be called either with an err or without it to ensure further error handling by Express error middleware or further code execution or otherwise the process will hang. In case if there are no error listeners, error will be handled by Express error middleware as a developer error.

 // Readable stream
 module.exports.streamNoErrorListener = (req, res, next) => {
     const src = fs.createReadStream('url-does-not-exists/data.json');
     src.on('error', (err) => {
         next(err); // pass error to express
     });
     src.pipe(res);
 };

 // Writable stream
 module.exports.streamWritable = (req, res, next) => {
     const writableStream = new Writable({
         // eslint-disable-next-line no-unused-vars
         write(chunk, encoding, callback) {
             this.emit('error', new Error('Error happened in a writable stream'));
         }
     });

     writableStream.on('error', (err) => {
         next(err); // pass error to express
     });

     writableStream.write('test 1', 'UTF-8');
 };

6. What happens if async error is not passed to next()?

Consider this case:

setTimeout(() => {
    throw new Error('Asynchronous Error. Server will crash!');
}, 100);

Express.js will not handler this error. Instead, uncaughtException process event will catch it and by default crash the server.

Override this handler to add custom logic. It is important to crash the app as it is not safe to resume normal operation after 'uncaughtException'.

process.on('uncaughtException', (err) => {
    winston.error(err);
    // eslint-disable-next-line no-process-exit
    process.exit(1);
});

List of resources:

Clone this wiki locally