How to set up Next.js error monitoring

Mar 18, 2025

Errors are an inevitable part of software development, but so is catching and fixing them. You can use error tracking in PostHog to help you do this.

To help you set this up, this tutorial details how to create a basic Next.js app, set up PostHog on both the front and backend, and then automatically capture errors that happen in both locations.

1. Creating a Next.js app

Start by ensuring Node.js is installed (version 18.0 or newer) then run the following command. Say no to TypeScript, yes to app router, and the defaults for other options.

Terminal
npx create-next-app@latest next-errors

Next, we can create our frontend which will have two parts:

  1. A button that throws an error
  2. A button that makes a request to our backend API

We can modify app/page.js to do this:

JavaScript
'use client'
import styles from "./page.module.css";
export default function Home() {
const handleErrorButtonClick = () => {
throw new Error("Frontend error");
}
const handleAPIButtonClick = async () => {
const response = await fetch("/api/test-error");
const data = await response.json();
console.log("data", data);
}
return (
<div className={styles.page}>
<h1>Welcome to our broken app</h1>
<button onClick={handleErrorButtonClick}>
Click me for an error
</button>
<button onClick={handleAPIButtonClick}>
Click me for a backend API error
</button>
</div>
);
}

Next, we need to set up our API. To do this, create a new api directory inside the app directory, a test-error directory inside that, and then a route.js file inside that. In this file, create a basic GET() function that throws an error like this:

JavaScript
// app/api/test-error/route.js
export async function GET() {
throw new Error('Backend API error')
}

Once saved, run npm run dev to see your new app in action. Click either of the buttons to see the errors they trigger.

Errors in our Next.js app

2. Setting up PostHog

To start, in your PostHog project settings under error tracking, toggle on Enable exception autocapture. Once done, go back to your app and install both posthog-js and posthog-node:

Terminal
npm i posthog-js posthog-node

Frontend setup

We'll set up PostHog in the frontend first. This starts by creating a providers.js file in the app directory. In it, we initialize PostHog with your project API key and host from your project settings and pass it to a PostHogProvider.

JavaScript
// app/providers.js
'use client'
import posthog from 'posthog-js'
import { PostHogProvider as PHProvider } from 'posthog-js/react'
import { useEffect } from 'react'
export function PostHogProvider({ children }) {
useEffect(() => {
posthog.init('<ph_project_api_key>', {
api_host: 'https://us.i.posthog.com',
defaults: '2025-05-24',
})
}, [])
return (
<PHProvider client={posthog}>
{children}
</PHProvider>
)
}

We then import this into layout.js and wrap our app in it like this:

JavaScript
import "./globals.css";
import { PostHogProvider } from "./providers";
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<PostHogProvider>
{children}
</PostHogProvider>
</body>
</html>
);
}

PostHog then begins to autocapture events and frontend errors. If you go back to your app and click the Click me for an error button, you'll see an $exception event captured into PostHog.

PostHog

Backend setup

For the backend, we can create a posthog-server.js file in the app directory. In it, initialize PostHog from posthog-node as a singleton with your project API key and host from your project settings. This looks like this:

JavaScript
// app/posthog-server.js
import { PostHog } from 'posthog-node'
let posthogInstance = null
export function getPostHogServer() {
if (!posthogInstance) {
posthogInstance = new PostHog(
'<ph_project_api_key>',
{
host: 'https://us.i.posthog.com',
flushAt: 1,
flushInterval: 0
}
)
}
return posthogInstance
}

We can then import this singleton wherever we need it in the backend. Unfortunately, this doesn't autocapture errors by default, so we have some more work to do.

3. Capturing errors

With both front and backend initializations set up, capturing errors with PostHog is as simple as calling captureException or capturing an $exception event.

posthog.captureException(error, additionalProperties)

Doing this for every possible error is a hassle though and we'll inevitably miss errors we're not expecting. Our frontend implementation automatically captures errors thrown and caught by onError and onUnhandledRejection listeners, but this doesn't cover everything.

To capture more, we can set up some more boundaries and instrumentation.

How to capture frontend render errors

To ensure all component errors are tracked, we can use the built-in error boundary system. This is done by creating an error.jsx file like this:

JavaScript
// app/error.jsx
'use client'
import { useEffect } from 'react'
import posthog from 'posthog-js'
export default function Error({
error,
reset,
}) {
useEffect(() => {
posthog.captureException(error)
}, [error])
return (
<div>
<h1>Something went wrong!</h1>
<p>We've logged this error and will look into it.</p>
<button onClick={() => reset()}>Try again</button>
</div>
)
}

This triggers when there is an error rendering your component. You can test this by setting up a useEffect in our page.js file that triggers a render error like this:

JavaScript
'use client'
import { useState, useEffect } from 'react'
// ... rest of your code
const [shouldError, setShouldError] = useState(false)
useEffect(() => {
setShouldError(true)
}, [])
if (shouldError) {
throw new Error('This is a test error')
}
// ... rest of your code

You can also create a similar global-error.jsx file to capture errors affecting the root layout or more granular error boundaries by adding error.jsx files to specific route segments.

How to automatically capture backend errors

Because backend requests in Next.js vary between server-side rendering, short-lived processes and more, we can't rely on exception autocapture.

Instead, we create a instrumentation.js file at the root of our project and set up an onRequestError handler there. Importantly, we to both check the request is running in the nodejs runtime to ensure PostHog works and get the distinct_id from the cookie to connect the error to a specific user.

This looks like this:

JavaScript
// instrumentation.js
export function register() {
// No-op for initialization
}
export const onRequestError = async (err, request, context) => {
if (process.env.NEXT_RUNTIME === 'nodejs') {
const { getPostHogServer } = require('./app/posthog-server')
const posthog = await getPostHogServer()
let distinctId = null
if (request.headers.cookie) {
const cookieString = request.headers.cookie
const postHogCookieMatch = cookieString.match(/ph_phc_.*?_posthog=([^;]+)/)
if (postHogCookieMatch && postHogCookieMatch[1]) {
try {
const decodedCookie = decodeURIComponent(postHogCookieMatch[1])
const postHogData = JSON.parse(decodedCookie)
distinctId = postHogData.distinct_id
} catch (e) {
console.error('Error parsing PostHog cookie:', e)
}
}
}
await posthog.captureException(err, distinctId || undefined)
}
}

Now, when you click the Click me for a backend API error button, it will trigger an error which will be automatically captured by PostHog.

PostHog

4. Monitoring errors in PostHog

Once you've set up error capture in your app, you can head to the error tracking tab in PostHog to review the issues popping up along with their frequency.

PostHog

You can click into any of these errors to get more details on them, including a stack trace as well as archive, resolve, or suppress them. On top of this, you can analyze $exception events like you would any event in PostHog, including setting up trends for them and querying them with SQL.

5. Uploading source maps

PostHog uses source maps to show unminified code in your stack traces (so you can find where the errors are coming from). Next.js disables source maps by default during production builds to prevent you from leaking your source on the client, but you can opt-in with the productionBrowserSourceMaps configuration flag.

To enable this, we set a environment variable to control this so we only generate source maps when building locally (or if you are running a build in CI).

file=.env.local
GENERATE_SOURCEMAPS=true

Once we have the environment variable set, we can configure Next.js to generate source maps.

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
productionBrowserSourceMaps: process.env.GENERATE_SOURCEMAPS === 'true'
};
export default nextConfig;

Once you've done this, run npm run build to build your app and create a .next directory.

Using PostHog's CLI to inject and upload source maps

First, install the CLI:

Install posthog-cli

npm install -g @posthog/cli

To authenticate the CLI, you can call the login command and follow the instructions:

Terminal
posthog-cli login

If you are using the CLI in a CI/CD environment such as GitHub Actions, you can set environment variables to authenticate. POSTHOG_CLI_ENV_ID and POSTHOG_CLI_TOKEN should be the number in your PostHog homepage URL and a personal API key respectively.

Uploading source maps requires the error tracking write scope when creating a personal API key.

Use the --host option in subsequent commands to specify a different PostHog instance / or region. For EU users:

Terminal
posthog-cli --host https://eu.posthog.com [CMD]

Once you're authenticated, inject the context required by PostHog to associate the maps with the served code.

Terminal
# Inject metadata in files to resolve errors
posthog-cli sourcemap inject --directory ./path/to/assets

Finally, upload the modified assets to PostHog.

Terminal
# Upload assets to posthog
posthog-cli sourcemap upload --directory ./path/to/assets

You must also ensure that the modified asset bundles uploaded to PostHog are the ones your site serves. If you serve a copy of the bundled assets as they were prior to running posthog-cli sourcemap inject, we won't be able to use the uploaded sourcemap to unminify or demangle your stack traces.

Setting up a CI step to build your app and upload the source maps is a good way to ensure this.

Questions? Ask Max AI.

It's easier than reading through 663 pages of documentation

Comments