Page Content

Tutorials

What is Middleware for Error-Handling in Express?

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

  1. Errors thrown in synchronous code can be caught using a standard try...catch block.
  2. The try block contains the code that might throw an error, and the catch block executes if an error is thrown.
  3. You can modify the error object’s properties (e.g., error.message) and then re-throw it or handle it.

Asynchronous Code Errors (Callbacks)

  1. Node.js commonly uses the “error-first callback” pattern, where the first parameter of any callback function is the error object (err, data).
  2. If there’s no error, this parameter is null. If an error occurs, it contains a description and other information.
  3. 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).
  4. 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 for error 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)

  1. Promises handle errors differently than synchronous or callback-driven code.
  2. 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.
  3. 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.
  4. The async/await syntax, available from Node.js 8 by default, allows for try-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 before app.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!

Index