Skip to main content

Sentry Should Be Optional in Your Next.js App

A shield icon protecting a circuit board
Apr 10, 20261 min readSentry, Next.js, TypeScript

The problem

Sentry's Next.js SDK initializes in three places: instrumentation-client.ts (browser), sentry.server.config.ts (Node), and sentry.edge.config.ts (edge runtime). Each one calls Sentry.init() at module scope, and when the DSN environment variable is missing, the SDK throws and spews errors into the console on every request.

That's fine in production, where the DSN is always set. On local dev, a fresh clone, or a CI job that doesn't need error monitoring, it just makes the terminal unreadable.

One boolean, three files

The fix is a single exported constant in the shared config:

// lib/sentry.config.ts
export const sentryEnabled = !!publicEnv.NEXT_PUBLIC_SENTRY_CONFIG_ID;

Each initialization file wraps its Sentry.init() call in the guard:

// sentry.server.config.ts
import { sentryEnabled, sharedSentryConfig } from '@/lib/sentry.config';
 
if (sentryEnabled) {
  Sentry.init({ ...sharedSentryConfig });
}

The edge config is identical. The client config follows the same pattern, just with a bit more work inside the guard: it defers integration loading via requestIdleCallback so it doesn't block LCP.

The router transition export

Next.js 16 expects instrumentation-client.ts to export onRouterTransitionStart for Sentry's router instrumentation. When Sentry's off, that export should be undefined so Next.js skips it:

export const onRouterTransitionStart = sentryEnabled
  ? Sentry.captureRouterTransitionStart
  : undefined;

That keeps Next.js from calling Sentry.captureRouterTransitionStart when the SDK was never initialized, which would throw at runtime.

The takeaway

Any SDK that initializes at module scope should be guarded behind the presence of its config. One boolean in a shared config file is a lot cheaper than an evening spent figuring out why local dev is spewing errors, and it keeps the "works on a fresh clone" promise honest.