Arrow Web is part of the Arrow family and the successor of Node.ACS. It allows you to add additional public routes to an Arrow Builder Application to serve as front-ends to pretty much anything from a custom CMS to manage ArrowDB data or websites consuming Arrow APIs like we've done for the University, Developer Portal and many other of our properties.
Arrow uses the popular Express.
By default, your Arrow Web routes will use the 404 template of the Arrow Admin:
And if your route throws or calls next()
with an Error your users will see:
Since Arrow 1.7.10 (part of Appcelerator CLI 5.0.2) you can disable the default 404 page in by setting admin.disableDefault404:false
in one of your configuration files.
With this disabled your 404's will now look like:
Different? Yes. What you want? Probably not. So let's customise that..
In our app.js we require add two middleware, after Arrow has started
and initialised all routes. To keep our code clean we export these middleware from a new lib/middleware.js.
In our pageNotFound middleware we check for res.bodyFlushed
. In Arrow we don't send the response to the client right away so that you can use Arrow Blocks to modify the response. For Arrow Web routes calling res.render()
will send the response. In both cases this flag will be set once a response has been sent. Since our pageNotFound
is the second last middleware we can assume no route matched the request if this property has not been set yet. tl;dr it's a four-o-four.
Instead of rendering a 404 template here, we call next()
which accepts an error (of any type) as its argument. Which brings us to..
Whenever routing or middleware functions call next()
with an error, all remaining middleware will be skipped until it finds middleware that accepts four instead of three arguments, so-called error-handling middleware functions.
Like our errorHandler middleware.
You can pretty much do whatever you want in custom error handler or handlers. Log the error, notify devops and of course display a friendly message to the user.
As you explore our example there's a few things to pay attention to:
- This handler accepts strings and plain objects so that you can throw or call
next()
with a string or plain object with a message, status and/or code. Be aware that this won't get you a stack trace. We'll get back to a better solution for that. - The handler again checks
res.bodyFlushed
. It will always log the error, but once a response has been sent we cannot communicate with the client anymore. - The handler checks
req.xhr
and returns JSON in case the client usedXMLHttpRequest
, e.g. via jQuery. - When the request does not accept HTML we pass the error on to the builtin error handler. The client is probably calling an Arrow Builder API.
- If we're in a development environment, the handler will collect additional details to help the developer debug the error.
- Finally, we pass everything we know on to the renderer. Notice this works slightly different from how you'd normally work in Arrow or Express routes because of how Arrow buffers the response as we discussed before. We don't call
res.render()
butreq.server.app.render()
and then send the rendered template to the client. Make sure you handle the edge case where the template itself might produce an error.
To test a 404, simply go to /undefined
or any other URL that does not exist:
To test throwing or calling next()
with errors, I've wired up the /example route with 2 parameters you can set via the query string:
action
can bethrow
,next
and will default torender
. The values pretty much speak for themselves.type
is what would be thrown or passed on tonext()
. This can beError
,Object
or (the default)String
, but alsoHttpError
.
For example if we call /example?action=throw&type=Error
you will get:
As you can see in the error template the stack trace, request and environment information will not be visible in production:
I'm a lazy developer so instead of:
var e = new Error('Unauthorized');
e.status = 401;
return next(e);
I rather do:
return next({status:401, message: 'Unauthorized'});
But as we discussed this won't get us a Stack Trace. The proper way to deal with this are Custom Error Types that extend Error.
This is what the /example uses when you call it with type=HttpError
. You can find this custom error type in lib/HttpError.js. As you can see I follow Mozilla's guide and override the constructor to accept an Http Status Code as well as an optional error code.
There's two things I do different from Mozilla. Like their guide we generate the stack trace by creating a new Error()
within the constructor. But by passing the message to it we make sure that gets includes in the first line of the stack as well. Because we generate the stack within the constructor the first line (last step) of the trace will always lead to the constructor. I find this confusing so I use a simple RegExp to remove that line.
Let's build some fancy Arrow apps with just-as-fancy error pages!
Code Strong! 🚀