Error tracking for Next.js apps with Hawk. Catches errors in the browser, on the server and in the Edge runtime, and sends releases with source maps, so errors from the browser show the original source code instead of minified chunks.
- π Errors from the browser, Node.js and Edge runtimes with one import
- π§ Errors of server components, route handlers, server actions and middleware with the route data
- π§± Ready error pages and error boundary for React
- π
redirect(),notFound()and other Next.js control flow errors are skipped - β±οΈ Events are delivered before serverless functions are frozen
- π Client source maps sending
- β‘ Turbopack and webpack support
- π·οΈ Automatic release id from CI, git or build time
- π¬ Recent commits sending
- π Source maps removal after sending, so the source code is not published
- π‘οΈ Build never fails because of sending errors
yarn add @hawk.so/nextjsInstall it as a regular dependency, not with -D: next start loads next.config too, and fails with Cannot find module '@hawk.so/nextjs/config' when production installs skip dev dependencies.
Put your project's Integration Token to the NEXT_PUBLIC_HAWK_TOKEN environment variable of the build, e.g. in .env:
NEXT_PUBLIC_HAWK_TOKEN=eyJpbnRlZ3JhdGlvbklk...The plugin and the catchers read the same variable. The browser catcher has the token in the client code anyway, and Next.js inlines NEXT_PUBLIC_ variables during the build, so the running app needs no Hawk variables.
Then add three files. Releases with source maps are sent from the Next.js config, see Sending releases.
1. instrumentation-client.ts in the project root (or in src) catches errors in the browser. Next.js runs it before the app starts:
import * as Hawk from '@hawk.so/nextjs';
Hawk.init();
export const onRouterTransitionStart = Hawk.onRouterTransitionStart;2. instrumentation.ts catches errors on the server, in Node.js and Edge runtimes:
import * as Hawk from '@hawk.so/nextjs';
export function register() {
Hawk.init();
}
export const onRequestError = Hawk.onRequestError;3. app/global-error.tsx catches errors of the root layout and shows a minimal error page:
'use client';
export { HawkGlobalError as default } from '@hawk.so/nextjs';Next.js only logs errors shown by error pages, so send them from your error.tsx files with useHawkError:
'use client';
import { useHawkError } from '@hawk.so/nextjs';
export default function ErrorPage({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
useHawkError(error);
return <button type="button" onClick={reset}>Try again</button>;
}See the example app with all of them.
| Error | Caught by |
|---|---|
| Browser: event handlers, effects, timers, unhandled rejections | init() in instrumentation-client.ts |
Errors shown by error.tsx |
useHawkError(error) |
| Errors of the root layout | HawkGlobalError in global-error.tsx |
| Errors of a component tree | <HawkErrorBoundary> |
| Server components, route handlers, server actions, middleware | onRequestError in instrumentation.ts |
| Uncaught exceptions and unhandled rejections of the Node.js server | init() in instrumentation.ts |
| Errors you catch yourself | Hawk.send(error) |
Next.js uses errors for redirect(), notFound(), forbidden() and dynamic rendering bailouts. They are part of the normal flow and are not sent.
Server errors reach the error pages in the browser with a digest and a generic message in production. They are already sent from the server, so useHawkError and the boundaries skip them. The server event has the same digest in addons.nextjs.digest, so you can find it by the digest shown on the page.
All functions are imported from @hawk.so/nextjs and work in the browser, on the server and in the Edge runtime: Next.js picks the build for each runtime.
| name | description |
|---|---|
init(options?) |
Creates the catcher. Call it in instrumentation-client.ts and in register() of instrumentation.ts. See options |
send(error, context?, user?) |
Sends an error or a message. user is used on the server only, in the browser use setUser() |
setUser(user), clearUser() |
Sets the user sent with every event, in the browser only |
setContext(context) |
Sets data sent with every event. On the server it is shared by all requests of the process |
breadcrumbs.add(breadcrumb), .get(), .clear() |
Actions that happened before the error, in the browser only. Clicks, requests and navigations are added automatically |
flush(timeout?) |
Waits for the events being sent, 2 seconds at most by default. Resolves with false on timeout |
onRequestError |
Export it from instrumentation.ts to send errors of server components, route handlers, server actions and middleware |
onRouterTransitionStart |
Export it from instrumentation-client.ts to add a breadcrumb for every navigation |
useHawkError(error) |
Sends the error passed to error.tsx |
HawkGlobalError |
Ready global-error.tsx |
HawkErrorBoundary |
Error boundary that sends errors of its children. Props: fallback (element or ({ error, reset }) => element) and onError(error, info) |
<HawkErrorBoundary fallback={({ reset }) => <button onClick={reset}>Reload the widget</button>}>
<Widget />
</HawkErrorBoundary>| name | type | description |
|---|---|---|
token |
string | Integration Token. Default is the NEXT_PUBLIC_HAWK_TOKEN environment variable, on the server HAWK_TOKEN is checked first. Without a token nothing is sent |
release |
string | Release id. Default is process.env.HAWK_RELEASE set by the plugin, see Release id |
context |
object | Data sent with every event |
beforeSend |
function | Filters or modifies events. Return the event to send it or false to drop it |
disableGlobalErrorsHandling |
boolean | Disables catching of uncaught errors and unhandled rejections |
collectorEndpoint |
string | Collector endpoint for self-hosted Hawk: WebSocket URL in the browser, HTTP URL on the server |
user |
object | Current user, in the browser only |
breadcrumbs |
false/object | Breadcrumbs options of @hawk.so/browser, in the browser only |
consoleTracking |
boolean | Sends recent console output with the events, in the browser only. Default is true |
issues |
false/object | Web Vitals and long tasks detection of @hawk.so/browser, in the browser only |
Pass different options to the browser and the server by calling init() with them in each file.
- The server handles requests of many users at once, so there is no current user:
setUser()is ignored. Pass the user with the event instead:Hawk.send(error, { orderId }, { id: user.id }). onRequestErrorwaits for sending and passes it towaitUntilwhere the platform supports it, e.g. on Vercel. If you callsend()right before a serverless function returns, wait for it withawait Hawk.flush().- Events have Next.js data in
addons.nextjs:runtime,routerKind,routePath,routeType,method,path,digestand thehost,refereranduser-agentheaders. Cookies, authorization and IP addresses are not sent. - The Edge runtime has no process events, so only errors passed to
onRequestErrorandsend()are sent.next startpasses errors of Edge routes toonRequestErrorof the Node.js runtime. - Hawk doesn't apply source maps to server errors yet, so their stack traces point to the compiled files in
.next/server.
Important
We strongly recommend setting up release sending. With the source maps from the release, Hawk shows fragments of your source code around browser errors instead of minified bundle chunks. Without this, it is awkward and difficult to understand the problem.
withHawkConfig sends the release on next build. Put it in next.config.ts, next.config.mjs or next.config.js: import it from @hawk.so/nextjs/config and wrap the config you export.
import type { NextConfig } from 'next';
import { withHawkConfig } from '@hawk.so/nextjs/config';
const nextConfig: NextConfig = {
// your config
};
export default withHawkConfig(nextConfig);If the file exports a function, wrap that function the same way: export default withHawkConfig((phase, { defaultConfig }) => ({ ... }), options).
On next build the plugin:
- resolves the release id and exposes it to your code as
process.env.HAWK_RELEASE - enables client source maps (
productionBrowserSourceMaps) unless you set it yourself - after compilation sends recent commits and client source maps to Hawk
- removes client source maps and
sourceMappingURLcomments from.next/static, so the source code is not published
next dev sends nothing.
They apply only to the release sent from this file. Pass them as the second argument of withHawkConfig:
export default withHawkConfig(nextConfig, {
commits: false,
});| name | type | required | description |
|---|---|---|---|
token |
string | optional | Your project's Integration Token. Default is the HAWK_TOKEN or NEXT_PUBLIC_HAWK_TOKEN environment variable. Without a token the release is not sent |
release |
string/number | optional | Unique identifier of the release. See Release id |
removeSourceMaps |
boolean | optional | Shows if the plugin should remove client source maps after sending. Default is true |
commits |
boolean/object | optional | Sends recent commits with the release. Default is true. Pass false to disable or { repo, number } to set the path to the git repository (default is the project directory) and the number of commits (default is 5) |
Release id links error events to the source maps and commits sent from the build. The plugin picks it once per build, in this order:
releaseoptionHAWK_RELEASEenvironment variable- commit SHA from the CI environment: GitHub Actions (
GITHUB_SHA), GitLab CI (CI_COMMIT_SHA), Vercel (VERCEL_GIT_COMMIT_SHA), Netlify, Cloudflare Pages, CircleCI and others - current commit:
git rev-parse HEAD - build start time, e.g.
2026-10-27T18:30:00.000Z
Whatever is picked, the source maps and your code get the same value: the plugin inlines it into the build as process.env.HAWK_RELEASE, and init() takes it from there. You don't need to set this variable when the app runs.
Production builds often run without git and CI variables, for example in a Docker build or a separate Kubernetes pod. Then the release is the build time. Source maps still work, but commits are not sent and the release name tells nothing about the deployed version. To get meaningful releases, pass the id from your pipeline with the HAWK_RELEASE environment variable:
ARG HAWK_RELEASE
RUN yarn builddocker build --build-arg HAWK_RELEASE=$(git rev-parse HEAD) .Or set the release option, for example to the version from package.json, which package managers expose while running yarn build or npm run build:
export default withHawkConfig(nextConfig, {
release: process.env.npm_package_version,
});A version is unique only if you bump it for every deploy, otherwise errors of different builds get into the same release.
process.env.HAWK_RELEASE is typed automatically when you import the package. If your config is a JavaScript file and you don't import the package in TypeScript files, add the declaration to tsconfig.json:
{
"compilerOptions": {
"types": ["@hawk.so/nextjs/global"]
}
}If you connected @hawk.so/browser and @hawk.so/nodejs yourself:
- In
next.config.ts(or.js/.mjs), wrap the exported config withwithHawkConfigfrom@hawk.so/nextjs/config. See Sending releases. - Replace the catchers in
instrumentation-client.tsandinstrumentation.tswith the files from Connect. - Replace
reportError(error)inerror.tsxwithuseHawkError(error). - Remove
@hawk.so/browserand@hawk.so/nodejsfrom your dependencies, andHAWK_TOKENif it has the same value asNEXT_PUBLIC_HAWK_TOKEN.
- Next.js 15.4.1 or newer: the plugin uses the
compiler.runAfterProductionCompilebuild hook - React 18.2 or newer
- Node.js 20.9 or newer
- Hawk applies source maps to errors from the browser, so only client source maps are sent. Server source maps in
.next/serverare left untouched. - Releases are sent during the build. Make sure the build environment has the token and access to Hawk, and the build finishes before the new version gets traffic.
- When
experimental.sriis enabled,sourceMappingURLcomments are kept, since changing the files would break the integrity hashes. - Sending errors never fail the build: they are printed to the build output.
yarn # install dependencies
yarn lint # ESLint
yarn test # unit tests
yarn e2e # build and start the example app, check the release and the events received by a mock collectoryarn e2e opens the example in Chromium, install it once with yarn playwright install chromium. yarn e2e --debug-ids checks indexed source maps produced with turbopack.debugIds, yarn e2e --webpack checks the webpack build.