Error Tracking with Sentry in Express
How to integrate Sentry for error tracking in Node.js Express applications, including error handlers, performance monitoring, release tracking, and source maps.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
Sentry captures unhandled errors in your application, groups them by similarity, and sends alerts. It shows the stack trace, request context, user info, and breadcrumbs (events leading to the error). In Express, Sentry middleware wraps the request pipeline so any thrown error or unhandled rejection gets captured automatically.
When to Use
- Production applications where you need real-time error alerts
- Tracking error rates and regression detection across releases
- Capturing user-reported bugs with full stack traces and request context
- Monitoring performance (transaction tracing) alongside error tracking
- Triaging bugs by severity, frequency, and affected users
When NOT to Use
- Local development — use the debugger and console output
- Applications with no error handling — fix your try/catch blocks first
- Low-traffic internal tools — Sentry’s free tier has event limits
- Logging-only observability — Sentry is for errors, not general log shipping
Solution
Setup
npm install @sentry/node
Basic integration with Express
const express = require("express");
const Sentry = require("@sentry/node");
Sentry.init({
dsn: "https://your-dsn@sentry.io/123",
environment: process.env.NODE_ENV || "development",
tracesSampleRate: 0.1,
});
const app = express();
// Sentry request handler must be the first middleware
app.use(Sentry.Handlers.requestHandler());
app.get("/api/users/:id", (req, res) => {
if (req.params.id === "0") {
throw new Error("Invalid user ID");
}
res.json({ id: req.params.id, name: "Alice" });
});
// Sentry error handler must be after all routes
app.use(Sentry.Handlers.errorHandler());
app.listen(3000);
Manual error capture
Sentry.captureException(new Error("Something went wrong"));
Sentry.captureMessage("Rate limit approaching", "warning");
try {
JSON.parse("{invalid}");
} catch (err) {
Sentry.captureException(err);
}
Adding context to errors
// Set user context
Sentry.setUser({
id: 42,
email: "alice@example.com",
username: "alice",
});
// Set tags (filterable in Sentry UI)
Sentry.setTag("page_locale", "en-US");
Sentry.setTag("feature_flag", "new_checkout");
// Set extra context (searchable but not filterable)
Sentry.setExtra("order_id", "ord-123");
Sentry.setExtra("cart_items", 3);
// Clear user after logout
Sentry.setUser(null);
Breadcrumbs for debugging
Sentry.addBreadcrumb({
category: "auth",
message: "User authenticated",
level: "info",
data: { method: "oauth", provider: "google" },
});
Sentry.addBreadcrumb({
category: "http",
message: "GET /api/users/42",
level: "info",
data: { status_code: 200, duration_ms: 45 },
});
// If an error happens later, Sentry shows these breadcrumbs
Custom transaction for performance monitoring
const transaction = Sentry.startTransaction({
name: "process_order",
op: "function",
});
try {
const span = transaction.startChild({ op: "validate_payment" });
// ... validate payment ...
span.finish();
const dbSpan = transaction.startChild({ op: "save_order" });
// ... save to database ...
dbSpan.finish();
} catch (err) {
Sentry.captureException(err);
} finally {
transaction.finish();
}
Release tracking
Sentry.init({
dsn: "https://your-dsn@sentry.io/123",
release: process.env.SENTRY_RELEASE || "1.0.0",
environment: process.env.NODE_ENV,
});
// Sentry now groups errors by release and shows regression detection
Source maps for readable stack traces
npm install @sentry/cli --save-dev
// sentry.config.js
module.exports = {
org: "your-org",
project: "your-project",
authToken: process.env.SENTRY_AUTH_TOKEN,
release: process.env.SENTRY_RELEASE,
};
Upload source maps during build:
sentry-cli sourcemaps upload --release 1.0.0 ./dist
Or configure in sentry.properties:
defaults.url=https://sentry.io
defaults.org=your-org
defaults.project=your-project
auth.token=your-auth-token
Integrating with Winston
const winston = require("winston");
const Sentry = require("@sentry/node");
const logTransport = new winston.transports.Console();
const sentryTransport = {
log(info) {
if (info.level === "error") {
Sentry.captureException(new Error(info.message));
}
return true;
},
};
const logger = winston.createLogger({
transports: [logTransport, sentryTransport],
});
logger.error("Database connection failed");
// Sends to both console and Sentry
Filtering and sampling
Sentry.init({
dsn: "https://your-dsn@sentry.io/123",
tracesSampleRate: 0.1, // 10% of transactions
ignoreErrors: [
"ResizeObserver loop limit exceeded",
"Non-Error promise rejection captured",
],
denyUrls: [
/extensions\//i,
/safari-extension/i,
],
beforeSend(event) {
if (event.request.url.includes("/health")) {
return null; // Don't send health check errors
}
return event;
},
});
Variants
Using Sentry with Fastify
const Fastify = require("fastify");
const Sentry = require("@sentry/node");
Sentry.init({ dsn: "https://your-dsn@sentry.io/123" });
const fastify = Fastify();
fastify.addHook("onRequest", async (request, reply) => {
request.sentry = Sentry.getCurrentHub();
});
fastify.addHook("onError", async (request, reply, error) => {
Sentry.captureException(error);
});
fastify.get("/api/error", async () => {
throw new Error("Fastify error");
});
Using Sentry with TypeScript
import * as Sentry from "@sentry/node";
Sentry.init({
dsn: process.env.SENTRY_DSN,
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
],
tracesSampleRate: 0.1,
});
interface UserContext {
id: string;
email: string;
}
function setSentryUser(user: UserContext): void {
Sentry.setUser(user);
}
Best Practices
-
For a deeper guide, see Sentry: Error Tracking, Triage, and Resolution.
-
Place
Sentry.Handlers.requestHandler()before all other middleware — it needs to wrap the full request -
Place
Sentry.Handlers.errorHandler()after all routes — it catches errors that slip through -
Set
releaseto your git commit SHA or version tag — enables regression detection -
Upload source maps in CI — minified stack traces are useless without them
-
Use
beforeSendto filter noise (health checks, static asset 404s, browser extension errors) -
Set
tracesSampleRateto 0.1 in production — 1.0 overwhelms your Sentry quota -
Use
setUser()to identify who experienced the error — invaluable for support tickets -
Add breadcrumbs for key actions (auth, API calls, state changes) to trace the error path
Common Mistakes
- Not using the error handler middleware: without
Sentry.Handlers.errorHandler(), Express’s default error handler catches errors before Sentry. - Setting DSN in code: use an environment variable. Committing the DSN exposes it.
- Not uploading source maps: minified code produces unreadable stack traces. Always upload in CI.
- Sending too many events: without sampling or filtering, Sentry sends every error. Filter noise with
ignoreErrorsanddenyUrls. - Not clearing user context on logout: errors after logout are attributed to the previous user.
FAQ
How do I test that Sentry is working?
Create a test route:
app.get("/debug-sentry", () => {
throw new Error("Sentry test error");
});
Visit the route and check your Sentry dashboard for the error.
What is the difference between captureException and captureMessage?
captureException takes an Error object and includes the stack trace. captureMessage takes a string and creates a breadcrumb-level event. Use captureException for errors and captureMessage for warnings or info.
How do I handle async errors?
Sentry’s Express middleware catches async errors automatically in Express 5+. For Express 4, wrap async handlers:
const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
app.get("/api/async", asyncHandler(async (req, res) => {
const data = await fetchData();
res.json(data);
}));
Can I use Sentry for frontend errors too?
Yes. Install @sentry/react or @sentry/vue for frontend. Use the same organization and project — Sentry separates frontend and backend errors by platform.
How do I reduce Sentry costs?
- Lower
tracesSampleRate(0.01 for high-traffic apps) - Use
ignoreErrorsto filter known noise - Use
beforeSendto drop events from health checks and static assets - Set
maxBreadcrumbsto 20 instead of the default 100
Related Resources
High-Performance Logging with pino
How to use pino for fast structured JSON logging in Node.js, including log levels, child loggers, transports, and integration with Express and Fastify.
RecipeRotate Logs Daily with Winston
How to configure daily log rotation in Node.js using winston and winston-daily-rotate-file, including size limits, retention, compression, and transport combining.
RecipeStructured JSON Logging with structlog
How to emit structured JSON logs in Python using structlog, including context binding, log levels, processors, and integration with standard logging.