DEV Community

Wallace Freitas
Wallace Freitas

Posted on

Error Handling in Node.js: Patterns and Practices

A key component of creating dependable and durable Node.js apps is error handling. When errors are handled correctly, applications can avoid crashes, gracefully recover from unforeseen circumstances, and give users informative error messages. Using real-world TypeScript examples, we will examine popular error handling techniques and recommended practices in Node.js in this post.


Comprehending Node.js Errors

Errors in Node.js can happen at many levels:

Errors that arise when synchronous code is being executed are referred to as synchronous errors.

Asynchronous errors: Errors that arise during asynchronous processes, like async/await, promises, and callbacks.

Operational errors: Fixable mistakes like bad user input or network outages.

Programmer errors: Logic mistakes or undefined variables are examples of bugs in the code that are typically not fixable.

Synchronous Error Handling

Synchronous errors can be handled using try-catch blocks. Here's an example in TypeScript:

function divide(a: number, b: number): number {
    if (b === 0) {
        throw new Error("Division by zero is not allowed.");
    }
    return a / b;
}

try {
    const result = divide(10, 0);
    console.log(result);
} catch (error) {
    console.error("An error occurred:", error.message);
}
Enter fullscreen mode Exit fullscreen mode

Asynchronous Error Handling

Handling errors in asynchronous code can be more challenging. Let's look at different approaches:

1. Callbacks

In callback-based code, errors are usually passed as the first argument to the callback function.

import * as fs from 'fs';

fs.readFile('path/to/file', 'utf8', (err, data) => {
    if (err) {
        console.error("An error occurred:", err.message);
        return;
    }
    console.log(data);
});
Enter fullscreen mode Exit fullscreen mode

2. Promises

Promises provide a cleaner way to handle asynchronous errors.

import { readFile } from 'fs/promises';

readFile('path/to/file', 'utf8')
    .then(data => {
        console.log(data);
    })
    .catch(err => {
        console.error("An error occurred:", err.message);
    });
Enter fullscreen mode Exit fullscreen mode

3. Async/Await

Async/await syntax allows for more readable and maintainable asynchronous code.

import { readFile } from 'fs/promises';

async function readFileAsync() {
    try {
        const data = await readFile('path/to/file', 'utf8');
        console.log(data);
    } catch (err) {
        console.error("An error occurred:", err.message);
    }
}

readFileAsync();
Enter fullscreen mode Exit fullscreen mode

Centralized Error Handling

Centralized error handling can help manage errors consistently across your application. In an Express.js application, you can use middleware to handle errors.

1. Express Error Handling Middleware

import express, { Request, Response, NextFunction } from 'express';

const app = express();

app.get('/', (req: Request, res: Response) => {
    throw new Error("Something went wrong!");
});

app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
    console.error(err.stack);
    res.status(500).send({ message: err.message });
});

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});
Enter fullscreen mode Exit fullscreen mode

Graceful Shutdown

Handling errors that can cause your application to crash is important. Implementing a graceful shutdown ensures that your application cleans up resources and completes pending operations before exiting.

import http from 'http';
import express from 'express';

const app = express();
const server = http.createServer(app);

process.on('uncaughtException', (err) => {
    console.error('Uncaught Exception:', err.message);
    shutdown();
});

process.on('unhandledRejection', (reason, promise) => {
    console.error('Unhandled Rejection:', reason);
    shutdown();
});

function shutdown() {
    server.close(() => {
        console.log('Server closed');
        process.exit(1);
    });

    setTimeout(() => {
        console.error('Forced shutdown');
        process.exit(1);
    }, 10000);
}

app.get('/', (req, res) => {
    throw new Error("Unexpected error");
});

server.listen(3000, () => {
    console.log('Server is running on port 3000');
});
Enter fullscreen mode Exit fullscreen mode

Best Practices

Always handle errors: Never ignore errors. Always handle them appropriately.

Use error boundaries in React: If using React with Node.js, use error boundaries to catch errors in the component tree.

Log errors: Implement logging to capture detailed error information for debugging and monitoring.

Categorize errors: Differentiate between operational errors and programmer errors and handle them accordingly.

Fail gracefully: Design your application to fail gracefully, providing meaningful feedback to users and maintaining functionality where possible.


Conclusion

Handling errors well is essential to creating reliable Node.js apps. Adopting best practices and utilizing suitable patterns for both synchronous and asynchronous error handling will help you make sure that your application can handle mistakes gracefully and offer users an improved experience. Adding graceful shutdown and centralized error handling improves your application's dependability even more.

Adopt these techniques to create robust Node.js applications that can manage failures well and uphold strict reliability and user experience guidelines.

Top comments (0)