In 2 previous posts we explained how to create a custom error type and created a few custom HTTP errors we'll use to automatically send HTTP response to an error using Express.js error middleware and util functions. Our custom error types allow us to generically handle well-defined types of failure and auto-respond with appropriate HTTP status code and JSON data, if any is to be sent to the client.
To explain the direction we are headed, well known errors will be handled this way:
app.get('/user/me', function(req, res) {
db.getUser(...)
.then(user => res.json(user))
.catch(res.respond.notFound)
// or
.catch(error => res.respond.notFound(error, {reason: 'User ID not found'}))
})
Notice error handler functions passed to the .catch
methods. That is the API we want to construct to support automatic error handling, but also allow to send some context data alongside error in the HTTP response.
To get there, first we need to have something that will expose res.respond
API for every response. Ideal job for a tiny middleware function:
function attachResponder(req, res, next) {
res.respond = createResponder(req, res, next);
next();
}
We probably want our middleware to run before all other request handlers to expose the respond API for all requests. So it'll need to be set before any other handlers on a Express.js Router:
app.use(attachResponder)
// ...
app.get('/user/me', function(req, res) {
// ...
Great, logic to have res.respond
available on every request is here. But we haven't defined the logic behind its interface. Here it is:
function createResponder(req, res, next) {
const responder = {
_forwardError(error, ErrorClass = Error, data) {
const errorMessage = error instanceof Error ? error.message : error;
const errorToForward = new ErrorClass(errorMessage, data);
// forwards error to an error handler middleware
next(errorToForward);
},
badRequest(error, data) {
return responder._forwardError(error, HttpBadRequest, data);
},
notFound(error, data) {
return responder._forwardError(error, HttpNotFound, data);
},
internalServerError(error, data) {
return responder._forwardError(error, HttpInternalServer, data);
}
};
return responder;
}
Calling this function inside middleware function attaches responder object to res.respond
. Responder itself exposes 3 meaningful methods: badRequest, notFound and internalServerError. All 3 can be called without parameters, only with Error instance, only with error explanation(string) or with error and additional data we want to pass in the response.
_forwardError method mainly serves for code reuse. It resolves the error type and creates custom error type with appropriate message and data. That error is then forwarded to an Express.js error handler middleware.
Express.js error handling middleware is always declared as the last middleware on an Express.js Router. It needs to be able to receive and handle all the errors previous middleware functions might have thrown. It's pretty simple to create one that just ends the response with HTTP 500 error for every error it catches:
function errorHandler(error, req, res, next) {
res.sendStatus(httpResponseCodes.INTERNAL_SERVER_ERROR)
}
We'd attach it to an Express.js app like so:
app.use(attachResponder)
// ...
// app.get('/user/me', function(req, res) {
// ...
app.use(errorHandler)
But we don't want to respond with 500 – Internal server error
every time. Especially since we have our custom error types and their subtype. We can now do all sorts of smart logic inside this handler. We can recognize our custom errors, and since all of them have a status code and a message, just respond with that information. If you don't remember the custom error types we've defined you can look them up in the previous post.
function errorHandler(error, req, res, next) {
if (error instanceof HttpError) {
res.status(error.statusCode).json(error.data)
} else {
res.sendStatus(httpResponseCodes.INTERNAL_SERVER_ERROR)
}
}
We've reached the functional API we wanted from the start and we now have a logic that automatically processes well-defined error types and finishes the response gracefully. If we continued to add another error types that fall under the HttpError subtype it would all end up being automatically handled for us inside error handler middleware. Additional benefit of our custom errors going through the single point is that it lends itself great for logging/monitoring purposes. All other, generic errors in this case would just end up as an HTTP 500 error response.
Idea:
Responder API could be further extended to provide a means of responding to all kinds of request, even the successful ones. That is all omitted from this posts for the sake of brevity.
Top comments (1)
Wow this was a seriously good series of articles and a clever solution. Adding it to our young codebase now :)