SDK & Framework Guides

NestJS Error Tracking with Exception Filters

Centralize error reporting in NestJS with a global exception filter and the Sentry SDK, capturing every unhandled error across controllers.

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 before NestFactory.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.

Fix your next production error faster

Point any Sentry SDK at LightTrace — free up to 5,000 events/month.