best-practices-for-error-handling-in-typescript-with-expressjs.html

Best Practices for Error Handling in TypeScript with Express.js

Error handling is a critical aspect of building robust applications, especially when using Node.js frameworks like Express.js. When combined with TypeScript, a statically typed superset of JavaScript, error management becomes both more structured and predictable. This article outlines best practices for error handling in TypeScript with Express.js, providing you with actionable insights and code examples that you can implement in your applications.

Understanding Error Handling in Express.js

Error handling in Express.js is essential for ensuring that your application responds gracefully to unexpected situations. By properly managing errors, you can improve user experience, facilitate debugging, and maintain application stability.

Why Use TypeScript?

TypeScript offers several advantages over plain JavaScript, particularly in terms of type safety and improved tooling. The static type system allows developers to catch errors at compile time, which can significantly reduce runtime issues. Using TypeScript with Express.js enhances your error handling capabilities by allowing you to define custom error types and providing better autocompletion in IDEs.

Best Practices for Error Handling

1. Use Try-Catch for Synchronous Code

For synchronous operations, using a try-catch block can help you catch errors effectively. Here’s a simple example:

import express from 'express';

const app = express();

app.get('/sync', (req, res) => {
  try {
    // Simulate synchronous operation
    const result = riskyOperation();
    res.send(result);
  } catch (error) {
    handleError(res, error);
  }
});

2. Handle Asynchronous Errors with Promises

When dealing with asynchronous code, you can handle errors using .catch() on your promises. Here’s how you might do that:

app.get('/async', (req, res) => {
  riskyAsyncOperation()
    .then(result => res.send(result))
    .catch(error => handleError(res, error));
});

3. Use Async/Await with Try-Catch

The async/await syntax simplifies the handling of asynchronous code. It’s recommended to wrap your async routes in a try-catch block:

app.get('/async-await', async (req, res) => {
  try {
    const result = await riskyAsyncOperation();
    res.send(result);
  } catch (error) {
    handleError(res, error);
  }
});

4. Define Custom Error Classes

Creating custom error classes allows you to have more control over error handling. This helps in distinguishing between different types of errors:

class AppError extends Error {
  public statusCode: number;
  public isOperational: boolean;

  constructor(message: string, statusCode: number, isOperational = true) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = isOperational;
    Error.captureStackTrace(this, this.constructor);
  }
}

5. Centralized Error Handling Middleware

Express.js allows you to define custom error-handling middleware that can catch errors from all routes. Here’s how you can set it up:

app.use((err: AppError, req, res, next) => {
  const statusCode = err.statusCode || 500;
  res.status(statusCode).json({
    status: 'error',
    statusCode,
    message: err.message,
  });
});

6. Logging Errors

Logging errors is vital for debugging and monitoring the health of your application. You can use libraries like winston or morgan to log errors effectively. Here’s a basic setup using winston:

import winston from 'winston';

const logger = winston.createLogger({
  level: 'error',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log' }),
  ],
});

app.use((err: AppError, req, res, next) => {
  logger.error(err);
  next(err);
});

7. Sending User-Friendly Error Messages

While it’s essential to log detailed error information for developers, you should send user-friendly messages to the client to avoid exposing sensitive information. Ensure the response does not disclose internal error details:

app.use((err: AppError, req, res, next) => {
  const statusCode = err.statusCode || 500;
  const message = err.isOperational ? err.message : 'Something went wrong!';

  res.status(statusCode).json({
    status: 'error',
    message,
  });
});

8. Validating Input Data

Using libraries like Joi or express-validator can help validate incoming requests and catch errors before they reach your business logic:

import { body, validationResult } from 'express-validator';

app.post('/data', 
  body('email').isEmail(), 
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return handleError(res, new AppError('Invalid input', 400));
    }

    // Proceed with valid data
  }
);

Conclusion

Implementing robust error handling in TypeScript with Express.js not only improves the stability of your application but also enhances the user experience. By following these best practices—using try-catch, defining custom error classes, centralizing error handling, logging, and validating input—you can build a resilient application that gracefully handles errors.

Remember that effective error handling is an ongoing process, and continually refining your approach based on user feedback and application performance is key to success. Start implementing these strategies today to elevate your Express.js applications with TypeScript!

SR
Syed
Rizwan

About the Author

Syed Rizwan is a Machine Learning Engineer with 5 years of experience in AI, IoT, and Industrial Automation.