Error Response Formatting
⭐ Interview Importance: HIGH
⏱️ Revision Time: 14 min
Error Response Formatting ensures that no matter what kind of error occurs in your application, the client always receives a predictable JSON structure.
Overview
A common frustration for frontend developers is APIs that return errors in wildly different formats. Sometimes it’s { "message": "Failed" }, other times { "error_code": 123, "details": "Failed" }, and sometimes it’s just plain text "Internal Server Error".
In NestJS, you can use a Global Exception Filter to enforce a strict contract for all error responses across your entire architecture.
Key Concepts
- Standard Envelope: Creating a single TypeScript
interfacethat defines your error format. - Catching Everything: Using a global filter that catches
Error(the base class for almost everything in Node.js) to guarantee nothing slips through. - Context Injection: Adding helpful metadata to the error response, like the URL that failed or a timestamp, to aid in debugging.
Code Examples
1. Define the Standard Interface
First, define exactly what your frontend should expect.
// error-response.interface.ts
export interface StandardErrorResponse {
success: false;
statusCode: number;
message: string | string[]; // Can be a string or array of strings (for validation errors)
error: string; // E.g., 'Bad Request', 'Not Found'
timestamp: string;
path: string;
traceId?: string; // Helpful for distributed tracing (Datadog/NewRelic)
}
2. Implement the Formatter Filter
This filter acts as the final gatekeeper, translating both standard HttpExceptions and unknown Errors into your standard interface.
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { Request, Response } from 'express';
import { StandardErrorResponse } from './error-response.interface';
import { v4 as uuidv4 } from 'uuid'; // Generate unique trace IDs
@Catch()
export class ResponseFormattingFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
// 1. Determine Status Code
const status = exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
// 2. Extract Message
// HttpExceptions often contain detailed objects (e.g., from class-validator)
let message = 'Internal server error';
let errorType = 'Internal Error';
if (exception instanceof HttpException) {
const responsePayload = exception.getResponse();
if (typeof responsePayload === 'string') {
message = responsePayload;
} else if (typeof responsePayload === 'object' && responsePayload !== null) {
// Extract message array from class-validator payloads
message = (responsePayload as any).message || message;
errorType = (responsePayload as any).error || errorType;
}
}
// 3. Construct the Standardized Envelope
const errorResponse: StandardErrorResponse = {
success: false,
statusCode: status,
message: message,
error: errorType,
timestamp: new Date().toISOString(),
path: request.url,
traceId: uuidv4(), // Generate a unique ID so the user can report it to support
};
// 4. Log it (especially if it's a 500!)
if (status >= 500) {
console.error(`[TraceID: ${errorResponse.traceId}] CRITICAL ERROR:`, exception);
}
// 5. Send it
response.status(status).json(errorResponse);
}
}
Best Practices
- Keep it Simple: Don’t leak stack traces in your error formatting unless the
NODE_ENVis set todevelopment. In production, stack traces expose your infrastructure layout. - Trace IDs: Adding a
traceIdto your error format is an industry best practice. If a user gets an error, they can screenshot thetraceIdand send it to customer support. The support team can then search your backend logs for that exact string to find the exact stack trace that caused the user’s issue.