Error Handling in ExpressJSsteemCreated with Sketch.

in #node7 years ago



In this post, we see three ways of handling errors in an ExpressJS Server:

  • On-the-spot ExpressJS error handling
  • ExpressJS error handling middlewares
  • Making it easier with Boom HTTP-friendly error objects

hr

On-the-spot ExpressJS error handling


Error handling in ExpressJS can easily be done on the spot by setting the response status and sending an error message or maybe a more structured json error:

app.get('/categories/:categoryId', (req, res) => {
    Category.findById(req.params.categoryId, (err, category) => {
        if (err) {
            res.status(500).send('Something broke!');
            return;
        }
        if (!category) {
            res.status(404).json({ error: 'Not Found', message: 'Could not find Category of id ' + categoryId });
        } else {
            res.status(200).json(category);
        }
    });
});


hr

ExpressJS error handling middlewares


A better way to handle this would be to use a set of error handling middlewares:

app.use((err, req, res, next) => {
    Logger.log(err);
    next(err)
});
app.use((err, req, res, next) => {
  if (err instanceof AssertionError) {
    return res.status(400).json({
      type: 'AssertionError',
      message: err.message
    });
  }
  next(err);
});
app.use((err, req, res, next) => {
  if (err instanceof MongoError) {
    return res.status(503).json({
      type: 'MongoError',
      message: err.message
    });
  }
  next(err);
});
[...]
app.use((err, req, res, next) => {
    // Hide error details from end user
    return res.status(500).json(error: 'Internal Server Error', message: 'This error has been logged and sent to the Development team to improve our application! Sorry for any inconvenience...');
});


Remember that routes and middlewares are run in order. These error handling middlewares should be defined after all your other routes and middlewares. Our first middleware is logging all errors for further review by the Development Team. And the last middleware should be the default catch-them-all error handling middleware.

Our route above becomes:

app.get('/categories/:categoryId', (req, res) => {
    Category.findById(req.params.categoryId, function (err, category) {
        if (err) {
            next(err);
        }
        if (!category) {
            next(new NotFoundError('Not Found: Could not find Category of id ' + categoryId));
        } else {
            res.status(200).json(category);
        }
    });
});


Getting better, but defining all these Error Handling Middleware is a tedious process. We will also have to define NotFoundError and others Errors... It feels like we are reinventing the wheel...

hr

Making it easier with Boom HTTP-friendly error objects


Boom Node Module provides HTTP-friendly error objects that will make our Error Handling much easier!

Boom defines a bunch of HTTP Errors:

  • HTTP 4xx Errors:
    • Bad Request (400)
    • Unauthorized (401)
    • Payment Required (402)
    • Forbidden (403)
    • Not Found (404)
    • Method Not Allowed (405)
    • Not Acceptable (406)
    • Proxy Auth Required (407)
    • Client Timeout (408)
    • Conflict (409)
    • Resource Gone (410)
    • Length Required (411)
    • Precondition Failed (412)
    • Entity Too Large (413)
    • URI Too Long (414)
    • Unsupported Media Type (415)
    • Range Not Satisfiable (416)
    • Expectation Failed (417)
    • Teapot (418)
    • Bad Data (422)
    • Locked (423)
    • Failed Dependency (424)
    • Precondition Required (428)
    • Too Many Requests (429)
    • Illegal (451)
  • HTTP 5xx Errors:
    • Bad Implementation (500)
    • Not Implemented (501)
    • Bad Gateway (502)
    • Server Unavailable (503)
    • Gateway Timeout (504)

Yes, even the Teapot, error! 😂

Boom.teapot('sorry, no coffee...');
{
    "statusCode": 418,
    "error": "I'm a Teapot",
    "message": "Sorry, no coffee..."
}


Our error handling middlewares becomes:

app.use((err, req, res, next) => {
    Logger.log(err);
    next(err)
});
app.use((err, req, res, next) => {
    return res.status(err.output.statusCode).json(err.output.payload);
});


And our route becomes:

app.get('/categories/:categoryId', (req, res) => {
    if (!mongoose.Types.ObjectId.isValid(categoryId)) {
        return next(boom.badRequest("categoryId is not a valid ObjectId: " + categoryId));
    }
    Category.findById(categoryId, function (err, category) {
        if (err) {
            return next(boom.boomify(err, { message: "Category not found with id " + categoryId }));
        }

        if (!category) {
            return next(boom.notFound("Category not found with id " + categoryId));
        }

        req.category = category;
        return next();
    });
});


Easy!!!

And, another benefit of using Boom is that all Server Errors (Status Code >= 500) messages are hidden from the end user. Instead, it will generate the following payload:

{
    "statusCode": 500,
    "error": "Internal Server Error",
    "message": "An internal server error occurred"
}


You can still access the real error message for logging purpose but the output is sanitized.

hr

References