error.tsx` catches a failure and shows the user something reasonable. It does not tell you the failure happened at all unless you are actively watching. For a while my "monitoring" was a client messaging me that something was broken, which is not monitoring, it is finding out from the worst possible source.
Here is the Sentry setup I actually use now, tuned to catch what matters without burying it in noise.
1. The Setup
bash
npx @sentry/wizard@latest -i nextjs
The wizard generates the config files and wraps next.config.ts automatically. Worth reviewing what it creates rather than trusting it blindly, since the defaults capture more than most projects actually need.
`ts
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 0.1,
environment: process.env.NODE_ENV,
});
`
`ts
// sentry.server.config.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 0.1,
});
`
tracesSampleRate: 0.1 matters more than it looks like it should. Setting this to 1.0 captures full performance tracing on every single request, which sounds thorough and quickly becomes expensive and noisy once real traffic shows up. Ten percent is a reasonable starting point for most projects, adjustable once you see actual volume.
2. Connecting It to error.tsx
This is the piece that is easy to miss. error.tsx handles the user-facing fallback, but nothing about it reports the error anywhere by default.
`tsx
// app/dashboard/error.tsx
'use client';
import * as Sentry from '@sentry/nextjs';
import { useEffect } from 'react';
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
Sentry.captureException(error);
}, [error]);
return (
Something went wrong.
Try again
);
}
`
Without this useEffect, the error boundary works perfectly from the user's perspective, and you never find out it fired at all.
3. Capturing Errors in Server Actions
Server Actions that catch their own errors and return a typed result, the pattern from the form handling setup, are good UX. They are also silent by default, since a caught error never reaches an error boundary or Sentry unless you report it explicitly.
`ts
// actions/contact.ts
'use server';
import * as Sentry from '@sentry/nextjs';
import { ContactSchema } from '@/lib/validations/contact';
export async function submitContact(input: unknown) {
const parsed = ContactSchema.safeParse(input);
if (!parsed.success) {
return { success: false, message: 'Validation failed' };
}
try {
await db.messages.create({ data: parsed.data });
return { success: true, message: 'Message sent' };
} catch (error) {
Sentry.captureException(error, {
tags: { action: 'submitContact' },
});
return { success: false, message: 'Something went wrong' };
}
}
`
The tags: { action: 'submitContact' } matters once a project has more than a handful of Server Actions. Without it, every caught error shows up in Sentry as a generic database exception with no indication of which action actually triggered it.
4. Attaching User Context
An error report with no idea which user hit it is far harder to investigate, especially for something that only affects one account or one specific data condition.
`ts
// lib/auth.ts
import * as Sentry from '@sentry/nextjs';
export async function getSession(): Promise {
const session = await verifySessionFromCookie();
if (session) {
Sentry.setUser({ id: session.userId, email: session.email });
}
return session;
}
`
Setting this once, near where the session is already being read, means every subsequent error captured during that request automatically includes which user was affected, without needing to pass user info into every individual captureException call.
5. Filtering Out Noise Before It Reaches You
Not every thrown error deserves an alert. Bots hitting invalid routes, expected validation failures, browser extensions injecting broken scripts, all generate errors that are not actually actionable.
ts
// sentry.client.config.ts
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
ignoreErrors: [
'ResizeObserver loop limit exceeded', // harmless browser quirk
'Non-Error promise rejection captured', // usually a browser extension
],
beforeSend(event, hint) {
const error = hint.originalException;
if (error instanceof Error && error.message.includes('ChunkLoadError')) {
// Usually just a stale cached bundle after a new deploy, not a real bug
return null;
}
return event;
},
});
A monitoring setup that alerts on genuine noise trains you to ignore it, which defeats the purpose. Filtering these out up front keeps what does come through worth actually looking at.
6. Alerting on What Actually Matters
Sentry's default alerting fires on every new issue, which becomes unmanageable fast on any project with real traffic. In the Sentry dashboard, alert rules can scope to what genuinely needs immediate attention:
- Payment or checkout errors โ immediate alert, since this directly affects revenue
- Auth failures spiking above normal volume โ could indicate an attack or a broken deploy
- A new error type appearing for the first time โ worth a look, not necessarily urgent
- The same known issue recurring โ usually just a dashboard check, not a page
Scoping alerts by severity, rather than treating every captured exception as equally urgent, is what keeps monitoring useful instead of becoming another source of notifications everyone learns to dismiss.
7. What I Actually Monitor on Every Project
Server Action failures, tagged by which action, since these often represent a real user-facing failure that a route-level error boundary alone would not surface with enough detail.
Webhook handler failures, specifically Stripe and any other external service. A silently failing webhook means subscription status or payment state quietly goes out of sync with no visible symptom until a customer notices.
Auth and session errors, since these can indicate either a bug or actual malicious activity, and the distinction matters for how urgently to respond.
Anything inside a try/catch that was previously just logged to the console. Console logs disappear the moment a serverless function finishes executing. If it was worth catching and logging before, it is worth Sentry capturing now.
Summary
| Piece | Handles |
|---|---|
Sentry.captureException in error.tsx
|
Actually reporting errors the boundary already catches |
| Capturing inside Server Action try/catch blocks | Errors that are handled gracefully but still worth knowing about |
Sentry.setUser |
Knowing which account was affected without manual tagging everywhere |
ignoreErrors and beforeSend filtering |
Keeping noise out so real issues stand out |
| Scoped alert rules | Urgent alerts for payment/auth issues, not a notification for every exception |
| Tagging by action or route | Finding which specific piece of code actually failed |
The shift that mattered most for me: an error boundary or a caught exception handling a failure gracefully for the user does not mean the failure does not matter. It means the failure is invisible unless something explicitly reports it, which is the entire gap Sentry closes.
I run this exact setup, Sentry wired into error boundaries, Server Actions, and webhook handlers, on every client project that has actual users depending on it.
Get the templates: https://pixelanas.gumroad.com
Do you have real error monitoring set up, or find out about production bugs from users first? Drop it below ๐
Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751
Top comments (0)