Express

Handling Errors In ExpressJS Web Applications

admin  

You might be familiar with this type of errrors:

[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "null".] {
  code: 'ERR_UNHANDLED_REJECTION'
}

Those errors, UnhandledPromiseRejection, indicates that a promise in your JavaScript code was rejected, and this rejection was not properly handled. It can occur when there no .catch() handler attached to a promise, or when using async/await without a try-catch block to handle potential rejections.

When that happens it might be followed by:

[nodemon] app crashed - waiting for file changes before starting...

There are 2 reasons why we need to properly handle erros in express js applications:

  • to avoid server crashes. Obviously haveing an invalid request to retrieve a non existent item from a database should not crash the server.
  • to be able to identify the proper error. When errors are hidden inside promises and only a generic error is reported, it's hard to dig in the code to find the problem.

Handling Errors in Promises

When working with promises we need to make sure the error is caught along the code so it does not reach out in the express core code, because the default behavior is to report it and crash the server. The easiest way is to catch and handle potential errors when invoking the promise, using .then().catch() handlers or try{}catch(e){} blocks:

Using .catch() with Promises

doSomethingAsync()
  .then(result => {
    // Handle the successful result
    console.log(result);
  })
  .catch(error => {
    // Handle the error
    console.error('An error occurred:', error);
  });

Using try-catch with async/await

When using the async/await syntax, we should wrap your await calls in a try-catch block to catch any errors that occur during the promise execution:

async function asyncFunction() {
  try {
    const result = await doSomethingAsync();
    console.log(result);
  } catch (error) {
    console.error('An error occurred:', error);
  }
}

Refactoring existing express code to handle exceptions

Lets' assume we have the following promise that we invoke to get a specific item:

const getItem = (key) => {
        return new Promise((resolve, reject) => {
            const query = `SELECT * FROM ${table} WHERE item = ?`;
            this.db.get(query, [key], (err, row) => {
                if (err) {
                    reject(err);
                } else {
                    if (!row)
                        reject(err);
                    else{
                        row = mapRow(row);
                        resolve(row);
                    }
                }
            });
        });
    }

Then we invoke the function. When trying to retrieve an item which is not stored in the database if the promise is not handled, then the server will crash with an error.

router.get('/:item', async function(req, res) {
    const result = await getItem(req.params.item); 
    res.json(result); // Send the result back to the client
});

So, the first thing to do is to handle the error in the router.

Wrapping a try catch block around the promise call, when await is used

router.get('/:item', async function(req, res) {
    try {
        const result = await yourFunction(req.params.item); 
        res.json(result); 
    } catch (error) {
        console.error(error);
        // Send an HTTP 404 status code if no record is found, otherwise send a 500 for server errors
        if (error.message === 'No record found') {
            res.status(404).send({ message: error.message });
        } else {
            res.status(500).send({ message: 'Internal Server Error' });
        }
    }
});

If the function rejects the promise due to an error or because no record is found, the catch block will handle it. The client receives a 404 Not Found status if no record is found, indicating the specific issue, or a 500 Internal Server Error for any other errors, which is a more generic error response indicating something went wrong on the server.

Pros of using awaits in try/catch blocks

  • Readability: Code using await tends to be easier to read and understand, especially for those familiar with synchronous programming models. It allows you to write asynchronous code that looks and behaves like synchronous code.
  • Error Handling: With await, you can use traditional try-catch blocks for error handling, which can make your error handling logic clearer and more centralized.
  • Debugging: Debugging await/async code can be more straightforward since the call stack in debuggers often more accurately represents the flow of execution.
  • Grouping: Multiple calls can be handled in a single try catch block.

Cons

  • Scalability and Parallelism. Awaits do not perform well when we need to chain promises in complex flows or when we should run them in parallel.

Using .then() and .catch() in the Route Handler

router.get('/:item', function(req, res) {
    getItem(req.params.item)
        .then(row => { res.json(row); })
        .catch(error => {
            if (error.message === 'No record found') {
                // in this block we can diferentiante the log level when running in prod vs dev mode
                // as we don't want to show details in a real production environment
                res.status(404).send({ message: error.message });
            } else {
                console.error(error);
                res.status(500).send({ message: 'Internal Server Error' });
            }
        });
});

Pros

  • Flexibility: The .then() method can provide a more flexible way to handle complex chains of promises, especially when dealing with conditional branching or when each subsequent operation doesn't necessarily depend on the previous one.
  • Clarity: This approach makes the flow of asynchronous operations and their error handling very clear. Each .then() handles the success scenario, and .catch() at the end of the chain handles any errors that occur at any point in the promise chain.
  • Scalability for Non-Linear Flows: In cases where you have multiple asynchronous operations that can run in parallel, .then() combined with Promise.all() can be very efficient.
  • Error Propagation: A single .catch() can handle errors from any preceding .then() in the chain, simplifying the error handling logic.

Cons

  • Callback Hell: While .then() and .catch() can lead to more nested code compared to async/await, careful structuring and modularizing of your functions can keep your code clean and readable.
  • Error Handling: Make sure to always end your promise chains with a .catch() to avoid unhandled promise rejections.

Best Practice for Chained Promises

The choice between await and .then() should be guided by which makes your code more understandable and maintainable for you and your team, considering the specific context and complexity of your database operations:

  • Complexity and Readability: If your operations are highly sequential and the readability of your code is a priority, await might be the better choice. It's particularly beneficial when each operation logically follows from the result of the previous one.
  • Error Handling: If you prefer a unified way to handle errors via try-catch, then await is advantageous.
  • Parallel Operations: If multiple operations can be executed in parallel without waiting for each other, Promise.all() with .then() could be more efficient, though you can still use await with Promise.all() to achieve the same effect.

Chaining Multiple Promnises

  1. Using awaits
async function handleDatabaseOperations() {
    try {
        const result1 = await dbOperation1();
        const result2 = await dbOperation2(result1); // Assuming result1 is needed for result2
        // More operations can be chained similarly
        return finalResult; // Or handle the final result as needed
    } catch (error) {
        console.error(error);
        throw error; // Rethrow or handle error appropriately
    }
}
  1. Using then() catch()
function handleDatabaseOperations() {
    return dbOperation1()
        .then(result1 => dbOperation2(result1))
        // More .then() calls can be chained similarly
        .then(finalResult => {
            // Handle the final result
            return finalResult;
        })
        .catch(error => {
            console.error(error);
            throw error; // Rethrow or handle error appropriately
        });
}

Using util.promisify with Async/Await to handle callback-based functions

When using functions that are not promisified, we need to write wrapper promises over those functions to bring it to a unitary form, without implementing any logic there. As we introduce a level of abstractization that is not necesarry in many cases, we can avoid it and use util.promisify library. The util.promisify function converts a callback-based function (following the Node.js callback pattern) into a version that returns promises. This can make your code cleaner and avoid the explicit construction of new Promises.

First, convert the callback-based database query into a promise-returning function:

const util = require('util');

// Assuming `this.db.get` follows the Node.js callback pattern.
// Convert it into a function that returns a promise.
const dbGetAsync = util.promisify(this.db.get.bind(this.db));

// Now `dbGetAsync` can be used with async/await

Then, we use it directly in the route handler:

router.get('/:item', async function(req, res, next) {
    try {
        const query = `SELECT * FROM ${table} WHERE slug = ?`;
        const row = await dbGetAsync(query, [req.params.item]);
        if (!row) {
            const error = new Error('No record found');
            error.status = 404; // Add an HTTP status code to the error
            throw error; // Pass the error to the catch block
        }
        const result = mapRow(row);
        res.json(result);
    } catch (error) {
        next(error); // Pass the error to the next error handling middleware
    }
});

Using util.promisify has certain advantages, but in the same time it makes the code more tightly coupled. If in the future we need to replace the database with another connector then writng promises as an extra layer would be more usefull.

Using a Promise Library with Built-in Error Handling

If you're working extensively with promises, consider using libraries like Bluebird, which offer additional features for error handling and can simplify some aspects of catching errors that might otherwise be unhandled.

Centralizing Error Handling with Middleware

In Express, we can define an error-handling middleware that catches any errors passed to the next function. This allows us to handle all errors in one place, making the route handlers cleaner and more focused on their primary responsibilities.

We can add an error-handling middleware at the end of your middleware stack:

app.use(function(err, req, res, next) {
    console.error(err.stack);
    const statusCode = err.status || 500;
    const message = err.message || 'Internal Server Error';
    res.status(statusCode).send({ message });
});

This setup allows us to handle errors more cleanly in route handlers by using next(error) when an error occurs. The error-handling middleware will then catch and process this error, sending an appropriate response to the client.

Combining this with util.promisify simplifies working with asynchronous code by avoiding the manual wrapping of promises, and centralizing error handling through middleware reduces repetitive error-handling code in your route handlers, making applications more maintainable and scalable.

Handling Unhandled Promise Rejections and Uncaught Exceptions at process level

Unhandled promise rejections and uncaught exceptions are two distinct types of errors in Node.js and JavaScript applications that can lead to different kinds of issues, including crashing your application. Understanding and correctly handling both is crucial for maintaining the stability and reliability of your applications. Let's clarify these concepts and how to handle them:

Handling Unhandled Promise Rejections

An Unhandled Promise Rejection occurs when a promise is rejected (i.e., it results in an error), and no .catch() handler is attached to it, nor is it caught by an async/await try-catch block. This situation might lead to potential memory leaks, unexpected application behavior, or crash in future versions of Node.js. Starting with Node.js version 15.0.0, unhandled promise rejections terminate the Node.js process by default, although this behavior can be customized.

Using .catch() with Promises: Always attach a .catch() handler to your promises. Using try-catch with Async/Await: When using async/await, wrap your await calls in a try-catch block to handle any errors. Global Handler: You can listen for unhandled promise rejections globally using the process.on('unhandledRejection', handler) event. This should be used sparingly, mainly for logging and debugging, not as a primary error-handling strategy.

Use the process.on('unhandledRejection') event listener to catch unhandled promise rejections, which can otherwise lead to application crashes.

process.on('unhandledRejection', (reason, promise) => {
    console.error('Unhandled Rejection at:', promise, 'reason:', reason);
    // Handle the error here (e.g., log it, send email notification, etc.)
});

Note on Middleware and UnhandledPromiseRejection

Middleware in Express does not directly prevent UnhandledPromiseRejection from crashing the server, as these rejections may occur outside the request-response cycle. However, if an async function within a route handler or middleware doesn't catch a rejection (e.g., missing await or .catch()), it can lead to unhandled promise rejections. Ensuring that all asynchronous operations in your middleware and route handlers properly handle rejections will mitigate this risk.

In summary, while your error handling middleware is crucial for managing errors that occur during request processing, global handlers for unhandledRejection and uncaughtException are necessary to manage the broader class of errors and promise rejections that can occur in a Node.js application.

Handling Uncaught Exceptions

Similarly, catch uncaught exceptions with process.on('uncaughtException'). However, after logging the error or performing cleanup, you should consider restarting the application as it might be in an undefined state.

An Uncaught Exception occurs when an error is thrown synchronously in the application and is not caught by any try-catch blocks. Uncaught exceptions can lead to the immediate termination of the Node.js process, potentially causing your application to crash.

How to Handle:

Try-Catch: Use try-catch blocks around code that might throw exceptions. Global Handler: Node.js provides a way to catch all uncaught exceptions using process.on('uncaughtException', handler). This allows you to log the error, perform cleanup, and potentially keep the process running. However, be cautious; after an uncaught exception, your application might be in an undefined state, and it is often recommended to restart the process after logging the error and performing any necessary cleanup.

process.on('uncaughtException', (error) => {
    console.error('Uncaught Exception:', error);
    // Perform cleanup if necessary
    process.exit(1); // Exiting is recommended after handling the exception
});

An uncaught exception can potentially crash an Express server by causing the Node.js process to exit. This is because an uncaught exception may leave your application in an unpredictable state, making it unsafe to continue running.

Why Middleware Can't Catch Uncaught Exceptions

Middleware functions in Express are scoped to handle errors that occur in the request-response cycle and are passed along via the next(err) function. Since uncaught exceptions are, by definition, not caught by the application code, they do not propagate through the Express middleware stack. Therefore, while middleware is great for handling operational errors that you anticipate might occur during normal operations (like database errors, missing files, etc.), it is not suitable for catching programming errors that lead to uncaught exceptions.

Difference Between Unhandled Promise Rejections and Uncaught Exceptions

Origin: Unhandled promise rejections originate from promises that fail without a .catch() handler, while uncaught exceptions originate from synchronous code that throws an error outside of a try-catch block. Handling: Unhandled promise rejections are specifically tied to the Promise API and async/await syntax and are handled using .catch() or try-catch. In contrast, uncaught exceptions are caught using try-catch blocks or the uncaughtException event for errors thrown synchronously. Impact: Both can lead to application instability or crashes, but they come from different sources and require different handling strategies. Properly handling both unhandled promise rejections and uncaught exceptions is essential for creating resilient Node.js applications. It's also a good practice to use monitoring and alerting tools to detect these issues in production environments.

Note: Exiting the process on uncaughtException is often recommended because the application may be in an unknown state. Consider using a process manager like PM2, Forever, or Node.js cluster to restart the app automatically.

Best Practices to handle server crashes

  • Prevention: The best way to handle uncaught exceptions is to prevent them in the first place. This involves using try-catch blocks around synchronous code that might throw errors, properly chaining promises with .catch(), and using async-await with try-catch to handle errors in asynchronous code.
  • Graceful Shutdown: When an uncaught exception is caught using the process.on('uncaughtException') handler, it's often wise to perform a graceful shutdown of the server. This means closing database connections, stopping server listeners, and performing any needed cleanup operations before exiting the process. After these operations are complete, you can restart the process fresh.
  • Use Process Managers: Tools like PM2, Forever, or Docker can automatically restart your application if it crashes. This can help improve uptime but doesn't replace the need for proper error handling and logging within your application.

Using PM2 to restart server when crashes

Relying on a process manager like PM2 to restart your server when it crashes is a common practice in production environments for ensuring high availability. However, it's important to balance this approach with proper error handling within your application. Let's explore the considerations:

When Relying on PM2 is Appropriate

  • High Availability: In production, you want to minimize downtime. PM2 can automatically restart your application if it crashes, helping to ensure that your service remains available.
  • Unexpected Failures: For errors that are truly unexpected and indicate an unknown issue in your application (e.g., a third-party service suddenly behaving differently), restarting can be a quick way to recover from these failures.

The Importance of Proper Error Handling

  • Root Cause Analysis: Restarting on crash does not address the underlying cause of the crash. Without proper logging and error handling, you might miss critical information needed to diagnose and fix recurring issues. Resource Leaks and Corruption: Some errors can leave your application in a corrupted state or lead to resource leaks (e.g., open file descriptors, memory leaks). Restarting the server does not solve these underlying issues and, over time, might worsen the stability of your application.
  • User Experience: Frequent restarts can lead to a poor user experience, with requests failing unexpectedly. Proper error handling allows you to provide more meaningful feedback to users when something goes wrong. Security Implications: Some crashes could be induced by malicious users (e.g., through unhandled exceptions). Simply restarting the server without addressing the vulnerability can leave your application exposed to repeated attacks.

Best Practices

  • Graceful Error Handling: Use try-catch blocks, promise .catch() handlers, and process-level event listeners (uncaughtException, unhandledRejection) to manage errors. Log these errors and, where appropriate, return meaningful responses to users.
  • Logging and Monitoring: Implement comprehensive logging to capture errors and monitor your application’s health. Use these logs to identify and fix recurring issues.
  • Graceful Shutdowns: In the event of an uncaught exception, initiate a graceful shutdown where you can clean up resources properly before exiting. PM2 can then safely restart a clean instance of your application. Rate Limiting Restarts: Configure PM2 to limit the rate at which it restarts your application to avoid crash loops that can occur from persistent errors.

In summary, while PM2 or similar process managers are valuable tools for maintaining uptime, they should complement, not replace, proper error handling within your application. The goal should be to create a robust and resilient application where unhandled errors are exceptions, not the norm, and PM2 serves as a safety net for unforeseen issues rather than a primary error recovery mechanism.

    Express

Handling Errors In ExpressJS Web Applications

Mastering error handling in ExpressJS: Prevent crashes and enhance stability with effective strategies for managing unhandled rejections and exceptions.

Handling Errors In ExpressJS Web Applications

Mastering error handling in ExpressJS: Prevent crashes and enhance stability with effective strategies for managing unhandled rejections and exceptions.

Handling Errors In ExpressJS Web Applications

Mastering error handling in ExpressJS: Prevent crashes and enhance stability with effective strategies for managing unhandled rejections and exceptions.

Handling Errors In ExpressJS Web Applications

Mastering error handling in ExpressJS: Prevent crashes and enhance stability with effective strategies for managing unhandled rejections and exceptions.