NestJS's modular architecture makes it great for building large APIs, but exceptions can get lost in the layers. A controller throws, a service fails to catch it, middleware swallows it, or an async job silently dies — and you find out three days later when a user complains that payments aren't being processed. NestJS error handling with the Sentry SDK ensures every unhandled exception reaches your dashboard, grouped and actionable, so your team can fix it before it becomes a production incident.
This guide shows you how to set up production-grade error tracking in NestJS with a global exception filter and the Sentry SDK, pointed at LightTrace. By the end, you'll capture synchronous errors, async failures, and HTTP exceptions across all your controllers and services with full stack traces and request context.
Install and initialize the SDK
Start by installing the Sentry SDK for Node.js:
npm install @sentry/node
Initialize it early in your main.ts file, before NestJS creates the app:
import * as Sentry from "@sentry/node";
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
Sentry.init({
dsn: "https://<key>@your-lighttrace-host/1",
environment: process.env.NODE_ENV,
release: "api@1.2.3",
tracesSampleRate: 1.0,
});
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
That single init call installs global handlers for uncaught exceptions and unhandled promise rejections. But NestJS's exception flow is hierarchical — exceptions bubble up through filters — so you need a custom global exception filter to catch and report them.
Create a global exception filter
NestJS exception filters intercept exceptions at any level (controller, service, guard) and give you a chance to transform or log them before responding. Create a filter that reports to Sentry:
// src/filters/sentry-exception.filter.ts
import {
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
ArgumentsHost,
} from "@nestjs/common";
import * as Sentry from "@sentry/node";
@Catch()
export class SentryExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const request = ctx.getRequest();
const response = ctx.getResponse();
// Capture any exception that reaches here
Sentry.captureException(exception);
// Handle HttpException (thrown by NestJS guards, pipes, etc.)
if (exception instanceof HttpException) {
const status = exception.getStatus();
const message = exception.getResponse();
return response.status(status).json({
statusCode: status,
message,
timestamp: new Date().toISOString(),
});
}
// Handle unknown exceptions
response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
message: "Internal server error",
timestamp: new Date().toISOString(),
});
}
}
Now register it globally in your app:
// src/main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { SentryExceptionFilter } from "./filters/sentry-exception.filter";
import * as Sentry from "@sentry/node";
// ... Sentry.init() ...
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Apply the global exception filter
app.useGlobalFilters(new SentryExceptionFilter());
await app.listen(3000);
}
bootstrap();
Register the Sentry exception filter after you create the app but before you call listen(). That ensures every request flows through it.
Capture request context
Raw stack traces are useful, but the real debugging speed comes from context — which user hit the error, what endpoint, what data were they sending? Middleware is a great place to attach this to Sentry:
// src/middleware/sentry.middleware.ts
import { Injectable, NestMiddleware } from "@nestjs/common";
import { Request, Response, NextFunction } from "express";
import * as Sentry from "@sentry/node";
@Injectable()
export class SentryMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
// Set the user context if authenticated
if (req.user) {
Sentry.setUser({
id: req.user.id,
email: req.user.email,
username: req.user.username,
});
}
// Add a breadcrumb for the incoming request
Sentry.addBreadcrumb({
category: "http.request",
message: `${req.method} ${req.path}`,
level: "info",
data: {
method: req.method,
path: req.path,
query: req.query,
},
});
next();
}
}
Register it in your module:
// src/app.module.ts
import { Module, NestModule, MiddlewareConsumer } from "@nestjs/common";
import { SentryMiddleware } from "./middleware/sentry.middleware";
@Module({
imports: [],
controllers: [],
providers: [],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(SentryMiddleware).forRoutes("*");
}
}
Now every error arrives with the breadcrumbs leading up to it — the exact endpoint, method, query parameters, and the authenticated user. If a payment fails mid-checkout, you see the full path the request took.
Handle async errors and promises
NestJS services often use async/await, and unhandled promise rejections can slip past if you're not careful. Always either catch and rethrow, or let the exception filter catch them:
// src/services/payment.service.ts
import { Injectable } from "@nestjs/common";
import * as Sentry from "@sentry/node";
@Injectable()
export class PaymentService {
async processPayment(orderId: string) {
try {
Sentry.addBreadcrumb({
category: "payment",
message: "Processing payment",
level: "info",
data: { orderId },
});
const result = await this.stripe.charges.create({
amount: 10000,
currency: "usd",
});
Sentry.addBreadcrumb({
category: "payment",
message: "Payment succeeded",
level: "info",
data: { chargeId: result.id },
});
return result;
} catch (error) {
// Sentry will catch this when the exception filter runs
Sentry.addBreadcrumb({
category: "payment",
message: "Payment failed",
level: "error",
data: { orderId, error: error.message },
});
throw error;
}
}
}
When this service throws, the exception bubbles to your global filter, which captures it with all the breadcrumbs intact.
Monitor background jobs and scheduled tasks
If you're using @nestjs/schedule for cron jobs or other background work, wrap them with explicit error handling:
// src/tasks/email.task.ts
import { Injectable } from "@nestjs/common";
import { Cron } from "@nestjs/schedule";
import * as Sentry from "@sentry/node";
@Injectable()
export class EmailTaskService {
@Cron("0 * * * *") // Every hour
async sendPendingEmails() {
try {
Sentry.addBreadcrumb({
category: "task",
message: "Starting email task",
level: "info",
});
const emails = await this.db.emails.findMany({ sent: false });
for (const email of emails) {
await this.mailer.send(email);
}
Sentry.addBreadcrumb({
category: "task",
message: "Email task completed",
level: "info",
data: { count: emails.length },
});
} catch (error) {
Sentry.captureException(error, {
tags: {
task: "email",
taskType: "scheduled",
},
});
}
}
}
By tagging background job errors with the task name, you can later filter errors in LightTrace to see if a particular job is failing consistently.
Performance tracing for slow operations
Beyond capturing errors, distributed tracing shows you which endpoints and database queries are slow. Enable transaction tracing to see the critical path:
// src/main.ts
Sentry.init({
dsn: "https://<key>@your-lighttrace-host/1",
tracesSampleRate: 0.1, // Trace 10% of transactions
environment: process.env.NODE_ENV,
release: "api@1.2.3",
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
new Sentry.Integrations.OnUncaughtException(),
new Sentry.Integrations.OnUnhandledRejection(),
],
});
With tracing enabled, you can see which endpoints are slow and reduce API latency by fixing the bottleneck. If a database query is taking 2 seconds, the traces will show you exactly which query and how to optimize it.
Test and verify
To ensure your setup is working, create a test endpoint that throws:
// src/app.controller.ts
import { Controller, Get } from "@nestjs/common";
@Controller()
export class AppController {
@Get("/test-error")
testError() {
throw new Error("Test error to verify Sentry is working");
}
}
Visit http://localhost:3000/test-error, and within seconds you should see the error grouped in LightTrace. If you don't, check:
- That your DSN is correct (copy it from your LightTrace project settings)
- That
Sentry.init()runs beforeNestFactory.create() - That the global exception filter is registered before
app.listen() - That your environment is not accidentally filtering out the error
Some HTTP status codes (like 4xx) are treated as expected and may not trigger alerts by default. To verify, trigger a 500 error — those always get through.
What's next
Error tracking catches the exceptions; using them well is the next step. Set up alert rules to page your team on new issues or spikes in frequency. For large systems with multiple services, cross-project tracing stitches errors and spans across your entire architecture. And follow error tracking best practices to turn raw error feeds into a fast path to fixes.
Start tracking errors in minutes
Point the Sentry SDK at LightTrace and see your first NestJS errors grouped with request context in minutes — start free with 5,000 events per month.
Error tracking isn't optional for production NestJS. Silent failures in background jobs, async services, and long-running transactions are worse than crashes because you don't know they're happening. With the global exception filter in place and context attached to every error, every developer on your team can see exactly what went wrong and where — and your team can add error tracking to any other language or framework using the same Sentry SDK pattern.