Middleware for Error-Handling
The next middleware function in the application’s request-response cycle, the request object (req
), and the response object (res
) are all accessible to middleware functions in Express. Middleware functions designed specifically for error handling are unique because they take four arguments: (err, req, res, next)
. The err
parameter is an error object that is passed along when an error occurs.
How it Works
- The request-response cycle can be terminated, any code can be executed, the request and response objects can be modified, or the next middleware function in the stack can be called.
- If a standard middleware or route handler fails, it can call
next(err)
to transfer the error to the next error-handling middleware in the stack. - The middleware function stack of your application should have error-handling middleware defined at the very end. Before an error handler takes over, this guarantees that all other routes and middleware have had an opportunity to execute the request.
Types of Errors and How to Handle Them
Synchronous Code Errors
- Errors thrown in synchronous code can be caught using a standard
try...catch
block. - The
try
block contains the code that might throw an error, and thecatch
block executes if an error is thrown. - You can modify the error object’s properties (e.g.,
error.message
) and then re-throw it or handle it.
Asynchronous Code Errors (Callbacks)
- Node.js commonly uses the “error-first callback” pattern, where the first parameter of any callback function is the error object (
err, data
). - If there’s no error, this parameter is
null
. If an error occurs, it contains a description and other information. - It’s good practice to handle the error somehow, even if it’s just logging it, and to stop the function’s execution if an error is present (e.g., by returning).
- If an error event is emitted by an
EventEmitter
but not listened to, the Node.js program will crash. It’s considered best practice to always listen forerror
events on event emitters to gracefully handle them.
However, it’s vital to note that try...catch
will not catch exceptions thrown from asynchronous operations’ callbacks. This is a common pitfall in Node.js.
Code Example (Asynchronous Operation):
function doSomeAsynchronousOperation(req, res, cb) {
// imitating async operation
return setTimeout(function(){
cb(null, []);
},1000);
}
try {
// asynchronous code
doSomeAsynchronousOperation(req, res, function(err, rs){
throw new Error("async operation exception");
})
} catch(e) {
// Exception will not get handled here
console.log(e.message);
}
// The exception is unhandled and hence will cause application to break
Code Output:
<No output from catch block, application will crash with unhandled exception>
Asynchronous Code Errors (Promises)
- Promises handle errors differently than synchronous or callback-driven code.
- Errors thrown inside a promise that are not caught can lead to the error being “swallowed” (not reported), though this behaviour is deprecated in Node.js 8 in favour of terminating the process.
- You handle promise rejections using the
.catch()
method. The.catch()
method typically takes a single function as its argument, which serves as the error handler. - The
async/await
syntax, available from Node.js 8 by default, allows fortry-catch
blocks to handle errors in asynchronous operations, making the code appear more synchronous.
Async/Await Asynchronous code can be written in a more procedural, synchronous fashion with async/await
, introduced in Node.js 8 (and earlier with a flag) for Promises. The ability to handle errors with asynchronous code using ordinary try...catch
blocks is one of its strongest features.
Code Example:
const myFunc = async (req, res) => {
try {
const result = await somePromise(); // 'await' pauses execution until the Promise resolves
// ... further synchronous-looking code ...
} catch (err) {
// handle errors here, just like synchronous code
console.log(`An error occurred: ${err.message}`);
}
};
This greatly simplifies the writing and maintenance of intricate asynchronous logic. Even if a promise yields nothing, await
can still be used to finish the async
job. Until the promise is resolved or rejected, await
essentially halts the async
function’s execution.
Implementing a Unified Error Handler: One popular strategy is to put a unified error handler at the ultimate end of the routes and logic of your application. This middleware function will use next(err)
to capture any errors that are supplied to it.
For instance, a basic error handler might look like this:
app.use(function(err, req, res, next) {
console.error(err.stack); // Logs the error stack
res.status(err.status || 500); // Sets the HTTP status code (defaulting to 500 Internal Server Error)
res.render('error', { // Renders an error page, or sends JSON
message: err.message,
error: err
});
});
After then, this handler can render a ‘error’ view (views/error.pug
, for example) to show information such as the stack trace, status, and message.
Custom “404 Not Found” Pages in Express
When a user requests a URL that is not available on your server, you should show a personalised “404 Not Found” page. Express takes a particular approach to this:
- 404 responses are not handled as errors by Express by default, so your typical error-handling middleware (the one with four arguments) won’t catch them.
- Express can be set up to show a custom 404 page by adding a middleware function that manages all unhandled routes:
- You make use of the
app.use()
that takes as its first parameter a wildcard path (*
). This wildcard is compatible with all request paths. - In your
app.js
file, this particular 404-handling middleware needs to be the final route definition, just beforeapp.listen()
. This makes sure that it doesn’t run until all other defined routes such as/
,/users
,/about
, etc. have processed the request. It would catch all requests if you entered it earlier, stopping your real routes from functioning.
Example of a 404 Page Middleware:
// Example of existing routes
app.get('/', (req, res) => res.send('Welcome to the homepage!'));
app.get('/about', (req, res) => res.send('Learn about us.'));
// 404 handler (must be placed last)
app.use((req, res, next) => {
res.status(404); // Set the status code to 404
res.send('404 - Page Not Found'); // Send a simple message, or render a template
// Or, to render a custom HTML page:
// res.render('404', { title: '404', errorMessage: 'Oops! The page you're looking for does not exist.' });
});
// Start the server
app.listen(3000, () => {
console.log('Server is up on port 3000.');
});
Alternatively, if you prefer to have your unified error handler manage 404s, you can explicitly pass a 404 error to it:
app.use(function(req, res, next) {
next(new Error(404)); // Passes a 404 error to the next error handler
});
// Your 4-argument error handler (defined after all routes and general middleware)
app.use(function(err, req, res, next) {
if (err.message == 404) { // Check if the error is a 404
res.status(404).send('Custom 404 Error!');
} else {
// Handle other types of errors
res.status(err.status || 500).send('Something went wrong!');
}
});
Your dedicated error handler can handle all kinds of problems, including not-found pages, centrally using this method, where a general app.use()
captures unhandled requests and explicitly provides a 404 error.
These techniques will make your Node.js Express application more robust and intuitive in the event of unforeseen problems or requests for resources that don’t exist. Cheers!