Error-handling is one of the fortes of Koa.js. Using generator functions we don't need to deal with error handling in every level of the callbacks, avoiding the use of (err,
res
) signature callbacks popularized by Node.js. We don't even need to use the .error
or .catch
methods known to Promises. We can use plain old try/catch
that ships with JavaScript out of the box.
The implication of this is that we can now have the following centralized error handling middleware:
var logger = console; module.exports = function *(next) { try { yield next; } catch (err) { this.status = err.status || 500; this.body = err.message; this.app.emit('error', err, this); } };
When we include this as one of the first middlewares on the Koa stack, it will basically wrap the entire stack, which is yielded to downstream, in a giant try/catch
clause. Now we don't need to worry about exceptions being thrown into the ether. In fact, you are now encouraged to throw
common JavaScript errors, knowing that this middleware will gracefully unpack it for you, and present it to the client.
Now this may not always be exactly what you want though. For instance, if you try to upvote an ID that is not a valid BSON format, Mongoose will throw CastError
with the message Cast
to
ObjectId
failed
for
value
xxx
at
path
_id'
. While informative for you, it is pretty dirty for the client. So here's how you can override the error by returning a 400
error with a nice, clean message:
app.put('/links/:id/upvote', function *(next) { var link; try { link = yield model.upvote(this.params.id); } catch (err) { if (err.name === 'CastError') { this.throw(404, 'link can not be found'), } } // Check that a link document is returned this.assert(link, 404, 'link not found'), this.body = link; });
We basically catch the error where it happens, as opposed to let it bubble up all the way to the error handler. While we could throw a JavaScript error object with the status
and message
fields set to pass it along to the errorHandler middleware, we can also handle it here directly with the this.throw
helper of the Context object.
Now if you pass a valid BSON ID, but the link does not exist, Mongoose will not throw an error. Therefore, you still have to check whether the value of link
is not undefined
. Here is yet another gorgeous helper of the Context object: this.assert
. It basically asserts whether a condition is met, and if not, it will return a 400
error with the message link
not
found
, as passed in the second and third argument.
Here are a few more validations to the submission of links:
app.post('/links', function *(next) { this.assert(typeof this.request.body.title === 'string', 400, 'title is required'), this.assert(this.request.body.title.length > 0, 400, 'title is required'), this.assert(utils.isValidURL(this.request.body.URL), 400, 'URL is invalid'), // If the above assertion fails, the following code won't be executed. var link = yield model.create({ title: this.request.body.title, URL: this.request.body.URL }); this.body = link; });
We ensure that a title is being passed, as well as a valid URL, for which we use the following RegEx util:
module.exports = { isValidURL: function(url) { return /(ftp|http|https)://(w+:{0,1}w*@)?(S+)(:[0-9]+)?(/|/([w#!:.?+=&%@!-/]))?/; } };
Now there are still ways to refactor the validation checks into modular middleware; similar to what we did in Chapter 3, Multiplayer Game API – Connect this is left as an exercise to the reader.