Enhancing Error Logging in NestJS with Sentry
Error logs without context are just expensive noise. In a real NestJS system, we want logs that show what failed, where it failed, and who was affected.
This setup uses Sentry with a custom NestJS logger and a global exception filter, while keeping default console logging behavior.
Why It Matters
- Centralized error tracking reduces debugging time.
- Request and user context make incidents easier to reproduce.
- Global exception capture prevents silent failures.
- Console logging remains available for local and container logs.
Core Concepts
1. Install Sentry SDK
Install required package:
npm install @sentry/node
2. Custom Logger Extension
Extend ConsoleLogger and forward error/verbose logs to Sentry.
import { ConsoleLogger } from "@nestjs/common";
import * as Sentry from "@sentry/node";
export class SentryLogger extends ConsoleLogger {
error(message: unknown, ...optionalParams: unknown[]): void {
const errorMessage = String(message ?? "");
let stack: unknown = "";
let context = "";
if (optionalParams.length === 1) {
context = String(optionalParams[0] ?? "");
}
if (optionalParams.length >= 2) {
stack = optionalParams[0];
context = String(optionalParams[1] ?? "");
}
const formattedMessage = context ? `${context}: ${errorMessage}` : errorMessage;
Sentry.withScope((scope) => {
scope.setExtra("message", errorMessage);
scope.setExtra("context", context);
scope.setExtra("stack", stack);
Sentry.captureMessage(formattedMessage, "error");
});
super.error(errorMessage, ...(optionalParams as []));
}
verbose(message: unknown, ...optionalParams: unknown[]): void {
const verboseMessage = String(message ?? "");
const context = String(optionalParams[0] ?? "");
const extra = optionalParams.slice(1);
const formattedMessage = context ? `${context}: ${verboseMessage}` : verboseMessage;
Sentry.withScope((scope) => {
scope.setExtra("message", verboseMessage);
scope.setExtra("context", context);
scope.setExtra("extra", extra);
Sentry.captureMessage(formattedMessage, "info");
});
super.verbose(verboseMessage, ...(extra as []));
}
}
3. Global Exception Filter
Catch all unhandled exceptions and send enriched request context to Sentry.
import { Catch, type ArgumentsHost, type Provider } from "@nestjs/common";
import { APP_FILTER, BaseExceptionFilter } from "@nestjs/core";
import * as Sentry from "@sentry/node";
@Catch()
class SentryExceptionFilter extends BaseExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const http = host.switchToHttp();
const request = http.getRequest();
Sentry.withScope((scope) => {
if (request) {
scope.setTag("url", request.url);
scope.setTag("method", request.method);
scope.setTag("environment", process.env.NODE_ENV || "development");
scope.setExtra("request", {
url: request.url,
method: request.method,
headers: request.headers,
params: request.params,
query: request.query,
body: request.body ?? {},
});
if (request.user) {
scope.setUser({
id: request.user.userId,
email: request.user.userEmail,
username: request.user.userName,
});
scope.setExtra("userRole", request.user.userRole);
}
}
if (typeof exception === "object" && exception !== null && "response" in exception) {
const exceptionResponse = (exception as { response?: unknown }).response;
scope.setExtra("response", exceptionResponse);
}
Sentry.captureException(exception);
});
super.catch(exception as never, host);
}
}
export const SentryExceptionFilterProvider: Provider = {
provide: APP_FILTER,
useClass: SentryExceptionFilter,
};
4. Sentry Initialization in Bootstrap
Initialize Sentry once during app startup and register custom logger.
import { Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { NestFactory } from "@nestjs/core";
import { NestExpressApplication } from "@nestjs/platform-express";
import * as Sentry from "@sentry/node";
import { nodeProfilingIntegration } from "@sentry/profiling-node";
import { AppModule } from "./app.module";
import { SentryLogger } from "./utility/logger/sentry.logger";
const logger = new Logger("MyApp");
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
const configService = app.get(ConfigService);
Sentry.init({
dsn: configService.get<string>("SENTRY_DSN", ""),
environment: configService.get<string>("NODE_ENV", "development"),
tracesSampleRate: 1.0,
profilesSampleRate: 1.0,
normalizeDepth: 5,
integrations: [nodeProfilingIntegration()],
});
app.useLogger(new SentryLogger());
await app.listen(3000);
}
bootstrap()
.then(() => logger.log("Server is running"))
.catch((error) => logger.error("Bootstrap failed", error));
5. Register Filter in App Module
Register the global filter provider in AppModule.
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { ValidationProvider } from "./validation.provider";
import { SentryExceptionFilterProvider } from "./utility/logger/sentry-exception.filter";
@Module({
imports: [],
controllers: [AppController],
providers: [AppService, ValidationProvider, SentryExceptionFilterProvider],
})
export class AppModule {}
6. Logging Strategy
Use this layered strategy:
- Logger captures operational messages and error signals.
- Exception filter captures unhandled failures with request context.
- Sentry groups and tracks incidents across environments.
Practical Example
Typical project structure for this setup:
src/
utility/
logger/
sentry.logger.ts
sentry-exception.filter.ts
app.module.ts
main.ts
When a controller throws an unhandled error, the filter sends request/user metadata to Sentry, and the logger still writes to console output. Two observability channels, one less panic.
Common Mistakes
- Using
SENTRY_DNSinstead of correctSENTRY_DSNenvironment key. - Sending sensitive request data to Sentry without sanitization.
- Forgetting to register the global exception filter provider.
- Capturing only message strings and losing stack/context details.
- Running
tracesSampleRateat1.0in high-traffic production without budget planning.
Quick Recap
- Extend NestJS logger to forward important logs to Sentry.
- Add global exception filter for unhandled errors.
- Initialize Sentry at bootstrap with environment-based config.
- Register filter provider in module DI container.
- Keep logs contextual, structured, and privacy-aware.
Next Steps
- Add data scrubbing for secrets and personal data before sending events.
- Tune sampling rates by environment (
dev,staging,prod). - Add alert rules in Sentry for critical error thresholds.
- Add release tracking to map errors to deployments.
Top comments (3)
Thank you for this guide.
After applying it, I noticed that the log collector you wrote send all logs as Events / Errors to sentry and not to their log ingestion service: sentry.io/product/logs/
I wrote a custom logger and injected it to NestJS and it now works as expected:
Create a file for your logger:
Register it to be used by NestJS in your main.ts:
Thanks for the heads up and that fix, your custom logger tweak’s a solid upgrade over my janky one. Spot on, mate!
I think you can include this "--import ./instrument.mjs" from the dev and start commands (package.json), so you can initialize Sentry before starting to run this NestJS app as well