Rethrowing Errors: A Crucial Practice for Observability in Node.js Lambda Environments

In the intricate world of serverless computing, particularly within AWS Lambda environments, mastering error handling is paramount. One of the lesser-known but incredibly significant practices is the art of rethrowing errors. In this blog post, we’ll unravel the importance of rethrowing errors, focusing on why it’s a vital technique for enhancing observability in Node.js Lambda environments.

1. The Role of Observability in Serverless Architectures

Observability, the ability to understand what’s happening inside a system by examining its outputs, is a fundamental concept in modern software engineering. In serverless architectures like AWS Lambda, observability becomes even more critical due to the distributed and often ephemeral nature of functions.

2. Understanding the Significance of Rethrowing Errors

Rethrowing errors involves catching an error, enriching it with context, and then throwing it again. This seemingly simple act plays a pivotal role in enhancing observability:

3. Context Enrichment for Comprehensive Insights

When an error is rethrown, developers have the opportunity to enrich it with contextual information. This information might include details about the specific Lambda invocation, input parameters, timestamps, or even custom diagnostic data. By adding this context, errors become much more informative, aiding in rapid issue identification and resolution.

4. Centralized Error Aggregation and Analysis

Rethrowing errors facilitates centralized error handling. When errors are consistently rethrown at strategic points in the code, they can be aggregated and analyzed centrally. Centralized error logs allow developers to identify patterns, trends, and recurring issues, providing valuable insights into system behavior.

5. Enabling Real-Time Alerts and Notifications

By rethrowing errors, developers can implement real-time alerting mechanisms. Whenever specific errors occur, the system can immediately trigger notifications to designated channels, enabling instant responses. Real-time alerts are invaluable for proactive issue resolution and minimizing downtimes.

6. Simplifying Debugging and Troubleshooting

Rethrowing errors at appropriate stages in the code creates clear boundaries between different components of the application. When an error occurs, developers can quickly trace its origin and flow through the system. This simplifies debugging and troubleshooting processes, reducing the mean time to resolution (MTTR) significantly.

For the purpose of centralized error catching, we create a middleware using the middy npm package, and then we wrap our Lambda function handler with this middleware. This approach allows us to catch errors thrown by the Lambda function and handle them in a centralized manner. Here’s how it’s typically done:

//global-middleware.ts
import { ulid } from 'ulid';
import httpJsonBodyParser from '@middy/http-json-body-parser';
import middy from '@middy/core';
import { Handler } from 'aws-lambda';
import customErrorHandler from '../libs/error-handler';
import { LambdaHanderType } from '../types/shared-types';
import { validateSchema } from '../libs/schema-validator';

type MiddlewareOptions = {
  transactionName?: string;
  handerType?: LambdaHanderType;
  schema?: any;
};

const globalMiddleWare = <T extends Handler>(handler: T, options?: MiddlewareOptions) => {
  const handerType = options?.handerType ?? 'httpPost';

  const wrapped = middy(handler);
  if (handerType === 'httpPost') {
    wrapped.use(httpJsonBodyParser({}));
  }

  if (handerType === 'httpPost' && options?.schema) {
    wrapped.use({
      before: (request) => {
        const { body } = request.event;
        validateSchema(options.schema, body);
      },
    });
  }

  wrapped.use({
    onError: (errorHandler) => {
      const correlationId = ulid();
      const errorResponse = customErrorHandler(errorHandler.error, handerType, correlationId);
      return errorResponse;
    },
  });

  return wrapped;
};

export default globalMiddleWare;

An Example Lambda HTTP Function Handler Utilizing the Above Middleware for Centralized Error Handling. In the errorHandler you can use your logic for where to put this logs and whom you want to notify in case of serious errors.

// handler.ts
import { HTTP_STATUS_OK } from '../../shared/libs/constants';
import globalMiddleWare from '../../shared/middlewares/global-middleware';
import postSchema from '../../shared/schema/employees-post';
import { HttpEvent } from '../../shared/types/shared-types';
import { buildResponse } from '../../shared/utils/common.util';

type Employee = {
  name: string;
  email: string;
  department: number;
};

export const postHandler = async (event: HttpEvent<Employee>) => {
  const { body: requestPayload } = event;

  return buildResponse(HTTP_STATUS_OK, { ...requestPayload });
};

export const handler = globalMiddleWare(postHandler, {
  handerType: 'httpPost',
  transactionName: 'postEmployee',
  schema: postSchema,
});