How the TREK Server Handles Errors: A Dual-Stack Express and NestJS Error Management Strategy
The TREK server centralizes error handling through a unified middleware layer that converts every exception—whether from legacy Express routes or modern NestJS controllers—into a consistent JSON response containing an error field and the appropriate HTTP status code.
The TREK project (mauriceboe/TREK) implements a robust error handling strategy that bridges a legacy Express 4 application and a modern NestJS wrapper. Understanding how the TREK server handles errors is essential for debugging API failures and ensuring client applications receive predictable, actionable error responses regardless of which framework layer processes the request.
Unified Error Architecture
The server architecture maintains synchronization between two distinct frameworks. Rather than duplicating error logic, TREK employs a transparent error-handling layer that ensures both Express middleware and NestJS exception filters produce identical JSON payloads. This guarantees byte-identical responses across the entire API surface, preventing client-side parsing errors when the server migrates between route handlers.
Express Error Handling Middleware
Global Error Handler in server/src/app.ts
The foundation of TREK's error handling resides in the Express application entry point. After registering all routes and middleware, the server adds a catch-all error handler that intercepts any synchronous throws, rejected promises, or explicit next(err) calls:
// server/src/app.ts
app.use((err: unknown, _req, res, _next) => {
// Unwrap custom errors (e.g. ApiError) or default to 500
const status = err instanceof ApiError ? err.status : 500;
const message =
err instanceof Error ? err.message : 'Internal server error';
// Log the stack trace for debugging (only in non‑prod)
if (process.env.NODE_ENV !== 'production') console.error(err);
res.status(status).json({ error: message });
});
This middleware operates as the final safety net in the Express pipeline. It extracts HTTP status codes from custom ApiError instances while defaulting to 500 for unhandled exceptions, ensuring that asynchronous errors—caught via express-async-errors—never crash the server process.
The ApiError Custom Class
To propagate semantic HTTP status codes through the service layer, TREK defines a custom error class in the weather service module:
// server/src/services/weatherService.ts (line 507)
export class ApiError extends Error {
status: number;
constructor(status: number, message: string) {
super(message);
this.status = status;
}
}
Any service can instantiate new ApiError(404, 'Trip not found'), and the Express middleware automatically translates this into a 404 Not Found response with the JSON body { "error": "Trip not found" }. This pattern decouples business logic from HTTP transport concerns while maintaining type safety.
NestJS Exception Filter Implementation
TrekExceptionFilter in server/src/nest/common/trek-exception.filter.ts
When the server boots via the NestJS entry point (server/src/bootstrap.ts), the same error format is preserved through a dedicated exception filter. This filter implements the ExceptionFilter interface and mirrors the Express behavior while adding request-specific metadata:
// server/src/nest/common/trek-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class TrekExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
// Preserve the status from Nest’s HttpException, otherwise 500
const status =
exception instanceof HttpException
? exception.getStatus()
: (exception as any)?.status ?? 500;
const message =
exception instanceof Error ? exception.message : 'Internal server error';
// Optional logging (mirrors the Express logger)
if (process.env.NODE_ENV !== 'production') console.error(exception);
response.status(status).json({
error: message,
path: request.url,
timestamp: new Date().toISOString(),
});
}
}
The filter extracts the status property from ApiError instances (which also carry this field) or falls back to Nest's HttpException status. By maintaining implementation parity with the Express middleware, the server guarantees consistent error responses during framework migrations.
Global Registration in server/src/nest/app.module.ts
The filter is registered globally within the NestJS application module, ensuring that all controllers—including weather.controller.ts and trips.controller.ts—respond with the standardized JSON shape:
// server/src/nest/app.module.ts (conceptual registration)
// TrekExceptionFilter is applied globally to all controllers
This registration ensures that the NestJS layer cannot accidentally bypass the unified error format, even when throwing built-in exceptions like NotFoundException.
Error Flow Through the Stack
The following sequence illustrates how an error propagates from the database layer to the HTTP client:
- Service Layer Detection – A function such as
weatherService.getWeatherencounters an invalid location and throwsnew ApiError(404, 'Location not found'). - Route or Controller Propagation – The error bubbles out of the async handler.
- In the Express path,
express-async-errorsautomatically catches the promise rejection and forwards it to the global error-handling middleware. - In the Nest path, the exception bubbles to the global
TrekExceptionFilter.
- In the Express path,
- Error Handling Layer – The middleware or filter reads the
statusandmessageproperties, logs the error details (only in non-production environments), and dispatches the JSON response:HTTP/1.1 404 Not Found Content-Type: application/json { "error": "Location not found" }
Practical Error Handling Examples
Throwing a custom error from a service:
// server/src/services/weatherService.ts
if (!response.ok) {
throw new ApiError(response.status, data.reason ?? 'Open‑Meteo API error');
}
Express route delegation:
// server/src/routes/weather.ts (simplified)
router.get('/weather/:lat/:lng', async (req, res, next) => {
try {
const data = await getWeather(req.params.lat, req.params.lng);
res.json(data);
} catch (err) {
next(err); // delegated to the global error handler in app.ts
}
});
Nest controller with automatic filtering:
// server/src/nest/weather/weather.controller.ts
@Get(':lat/:lng')
async get(@Param('lat') lat: string, @Param('lng') lng: string) {
return await getWeather(lat, lng); // throws ApiError → TrekExceptionFilter
}
Environment-Based Error Logging
Both the Express middleware and the Nest filter implement identical environment checks to prevent information leakage. When NODE_ENV equals production, stack traces are suppressed and only the sanitized JSON error payload reaches the client. In development environments, console.error outputs full stack traces to facilitate debugging without requiring additional logging infrastructure.
Summary
- Unified JSON Format: All errors return
{ "error": "message" }regardless of whether they originate from Express or NestJS handlers. - Dual-Stack Compatibility: The
TrekExceptionFiltermirrors the Express middleware inserver/src/app.ts, ensuring consistent responses during framework transitions. - Custom ApiError Class: Defined in
server/src/services/weatherService.ts, this class enables HTTP status code propagation through the service layer. - Global Error Interception: Express uses a final middleware; NestJS uses a globally registered filter applied in
server/src/nest/app.module.ts. - Environment-Aware Logging: Stack traces are logged only in non-production environments to prevent sensitive data exposure.
Frequently Asked Questions
What JSON format does the TREK server return for errors?
The TREK server returns a JSON object containing a single error field with a human-readable message. For example: { "error": "Location not found" }. When handled by the NestJS layer, the response may additionally include path and timestamp fields for debugging purposes, though the core error message format remains identical to the Express implementation.
How does TREK handle asynchronous errors in Express routes?
The TREK server utilizes express-async-errors to automatically catch rejected promises from async route handlers. Without this package, unhandled promise rejections would crash the Node.js process; with it, they are forwarded to the global error-handling middleware in server/src/app.ts where they are processed identically to synchronous errors.
What is the purpose of the ApiError class in TREK?
The ApiError class, defined in server/src/services/weatherService.ts, extends the native JavaScript Error object to include a status property. This allows service layers to specify appropriate HTTP status codes (such as 404 for missing resources) without importing HTTP-specific libraries, keeping business logic decoupled from transport layer concerns while enabling the error handlers to return semantic status codes.
How does NestJS error handling differ from Express in the TREK server?
While both frameworks ultimately return the same JSON error format, NestJS utilizes the TrekExceptionFilter to intercept exceptions before they reach the Express layer. This filter handles Nest-specific HttpException types while also accounting for legacy ApiError instances. The Nest implementation provides additional request context (URL path and timestamp) but maintains the identical { "error": "message" } structure to ensure backward compatibility with existing API clients.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →