Skip to main content

advanced-error-handling-nodejs

Advanced Error Handling in TypeScript Node.js:

For each of these advanced TypeScript concepts, I'll provide an explanation and example:

  • Custom Error Classes:

    Extend the Error class to create specific error types.

    • Example:

      class DatabaseError extends Error {
      constructor(message: string, public code: number) {
      super(message);
      this.name = 'DatabaseError';
      }
      }

      class ValidationError extends Error {
      constructor(message: string, public fields: string[]) {
      super(message);
      this.name = 'ValidationError';
      }
      }
  • Typed Error Handling:

    Use type guards within catch blocks to handle errors based on their type.

    • Example:
      try {
      // some operation that might throw
      } catch (error) {
      if (error instanceof DatabaseError) {
      // handle database error
      } else if (error instanceof ValidationError) {
      // handle validation error
      } else {
      // handle generic or unknown error
      }
      }
  • Using instanceof for Error Checking:

    Determine the type of an error using instanceof.

    • Example:
      try {
      // operation that might throw
      } catch (error) {
      if (error instanceof DatabaseError) {
      console.error('Database error code:', error.code);
      }
      }
  • Union Types for Errors:

    Annotate functions with union types to represent all possible thrown errors.

    • Example:

      function riskyOperation(): void | never {
      throw new DatabaseError('Failed to connect', 500);
      }

      function handleOperation() {
      try {
      riskyOperation();
      } catch (error) {
      if (error instanceof DatabaseError || error instanceof ValidationError) {
      // handle known errors
      }
      }
      }
  • Error Boundary Middleware:

    Implement error-handling middleware in Express to catch and process errors from all routes.

    • Example:
      app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
      if (err instanceof DatabaseError) {
      res.status(500).send('Database error occurred');
      } else if (err instanceof ValidationError) {
      res.status(400).send('Validation failed');
      } else {
      res.status(500).send('An unexpected error occurred');
      }
      });
  • Async Error Handling:

    Use a higher-order function to wrap async functions and handle errors.

    • Example:

      const catchAsyncErrors = (fn: Function) =>
      (req: Request, res: Response, next: NextFunction) =>
      fn(req, res, next).catch(next);

      // Usage with an async route handler
      app.get('/data', catchAsyncErrors(async (req, res) => {
      // Async operations that might throw
      }));

Each of these strategies leverages TypeScript's type system to improve error handling, making your code more robust and easier to maintain.

Let's explore these concepts with explanations and examples:

  • Never Throwing Raw Errors:

    Use specific error types for throw statements to enhance error context.

    • Example:
      // Instead of throw new Error("Invalid input");
      throw new ValidationError('Invalid input', ['username', 'password']);
  • Error Logging:

    Implement logging that captures error details, including stack traces.

    • Example:
      function logError(error: Error) {
      console.error(error.message);
      console.error('Stack Trace:', error.stack);
      }
  • Global Exception Handling:

    Capture uncaught exceptions at the process level to manage unexpected errors.

    • Example:
      process.on('uncaughtException', (error) => {
      logError(error);
      process.exit(1); // Exit after logging
      });
  • Handling Promise Rejections:

    Listen for unhandled promise rejections and handle them globally.

    • Example:
      process.on('unhandledRejection', (reason, promise) => {
      logError(reason instanceof Error ? reason : new Error(reason));
      });
  • Type Guards for Error Properties:

    Check for custom properties on errors using type guards.

    • Example:
      catch (error) {
      if ('code' in error) {
      const errorCode = (error as { code: number }).code;
      // Handle based on errorCode
      }
      }
  • Rich Error Information:

    Add context to errors using additional properties.

    • Example:
      class NetworkError extends Error {
      constructor(message: string, public status: number, public data: any) {
      super(message);
      this.name = 'NetworkError';
      }
      }
  • Error Transformation for Clients:

    Format errors before sending them to client-side, removing sensitive data.

    • Example:
      function transformErrorForClient(error: Error) {
      return {
      message: error.message,
      // Exclude sensitive properties like 'stack'
      };
      }
  • Error Propagation:

    Decide whether to handle errors or let them bubble up to the caller.

    • Example:
      function mightFail() {
      try {
      // risky operation
      } catch (error) {
      if (shouldHandleLocally(error)) {
      handleLocally(error);
      } else {
      throw error; // propagate
      }
      }
      }
  • Testing Error Handling:

    Test error handling paths using mock errors and TypeScript's type assertions.

    • Example:
      describe('Error handling', () => {
      it('handles custom errors correctly', () => {
      const testError = new ValidationError('Test error', ['test']);
      expect(() => {
      throw testError;
      }).toThrow(ValidationError);
      });
      });

These practices help in creating a robust error handling system that utilizes TypeScript's static typing to manage and track errors effectively throughout the application.