What you'll learn
  • Why production crashes go unreported and how a tracker catches them anyway
  • What a DSN is and where the init call belongs in browser and server code
  • Why minified stack traces are unreadable and how source maps decode them
  • How grouping works and why users affected beats raw event count for triage
  • What the tracker covers that logs do not, and the other way around
on this page

The first time your deployed app crashes for a stranger, here is exactly what happens on your end: nothing. No alert, no email, no GitHub issue. The user taps a button, the page dies, they swear at their phone, and they leave. From where you sit, that silence is identical to everything working.

This is the part of shipping that toy projects never prepare you for. On localhost you are the only user, so you witness every crash personally. In production, the people hitting your bugs are strangers, and strangers owe you nothing. The bug reports you do get will come from friends, and they’ll arrive as a text that says “hey, your site’s broken on my phone,” with no further detail, two days after the fact.

An error tracker fixes this. It’s a catcher’s mitt for exceptions: every unhandled error in the browser or on the server gets caught and sent to a dashboard, with a stack trace and enough context to act on. Sentry is the name you’ll hear most. Bugsnag, Rollbar, Honeybadger, and the self-hostable GlitchTip all do the same job, and most have free tiers that cover a side project comfortably. This guide uses Sentry’s vocabulary, but the concepts transfer to all of them.

What gets caught, and what comes with it

The trick behind every error tracker is simple: the SDK (a small library you add to your app) hooks the global “something threw and nobody caught it” handlers. In the browser that’s window.onerror and the unhandledrejection event for promises; on a Node server it’s the equivalent process-level hooks. Which means it catches exactly the errors you forgot to catch. The ones you wrapped in a try/catch were never the problem.

Each capture is called an event, and an event is far more than the error message. It carries the stack trace, the browser and operating system (or Node version), the URL or route the user was on, the release version of your app, and a timestamp. The dashboard adds the two numbers that matter most: how often this error fires and how many distinct users it has hit.

That context is the whole value. “Broken on my phone” becomes: TypeError in OrderList.jsx line 24, Safari on iOS, route /orders, 43 users since Tuesday’s deploy. The first version is a shrug. The second is a fix you can start before lunch.

The setup is one init call

The install is genuinely small, which is why skipping it stings so much in hindsight. You create a project in the tracker’s dashboard and it hands you a DSN (Data Source Name): a URL that tells the SDK where to send events. It contains a public key that can only submit events, never read them, so it’s fine that anyone can see it in your shipped JavaScript. No secrets to guard here.

Then you call init as early as your code runs, before anything that might throw:

// src/main.js, the first thing your bundle executes
import * as Sentry from "@sentry/browser";

Sentry.init({
  dsn: "https://[email protected]/0",
  environment: "production",
  release: "[email protected]",
});

The environment field keeps your local crashes out of the production view (or you can skip init entirely in dev). The release string ties every error to a specific deploy, which matters more than it looks; it comes back in the source maps section.

On a server the rule is the same, stated more strictly: initialize before the rest of your app loads. The common pattern is a dedicated file you require at the very top of your entry point, or load with Node’s --import flag, so the hooks exist before any of your routes are even defined:

// instrument.js, loaded before everything else on the server
const Sentry = require("@sentry/node");

Sentry.init({
  dsn: "https://[email protected]/0",
  environment: "production",
  release: "[email protected]",
});

One framework wrinkle worth knowing: Express catches errors thrown in routes and turns them into a 500 response, which counts as “handled” from the SDK’s point of view. Sentry ships a one-line setup helper that hooks the error middleware so those still get reported. Whatever framework you use, its page in the Sentry docs shows the exact two or three lines; the shape is always init early, hook the framework, done.

Then break it on purpose. The official docs suggest exactly this: add a temporary route that throws, hit it, and watch the event land.

// temporary, delete after you've seen it work
app.get("/debug-sentry", () => {
  throw new Error("If you can read this in the dashboard, the mitt works");
});
[ prove the mitt works ]
npm install @sentry/node

Browser side is @sentry/browser, or the framework package like @sentry/react.

node server.js
API listening on port 3000
curl http://localhost:3000/debug-sentry
Internal Server Error

Within a few seconds a new issue appears on the dashboard. Break it deliberately once so you trust it when it breaks for real. Then delete the route.

Minified stack traces are gibberish on purpose

Your first real production error arrives, you open it expecting answers, and the stack trace looks like this:

TypeError: Cannot read properties of undefined (reading 'map')
    at a (index-BfK3x2.js:1:48211)
    at Wc (index-BfK3x2.js:1:120443)

Line 1, column 48,211, of a file that does not exist in your repo, in a function named a. Useless, and nobody broke anything. Your build minified the code: squashed it onto one line, renamed every variable and function to single letters, stripped the whitespace. Great for your users’ download size, fatal for stack traces.

Source maps are the decoder. A source map is a file your bundler can emit alongside each minified bundle, recording how every position in the squashed file maps back to the original file, line, and column. Hand the map to the tracker and the same error becomes:

TypeError: Cannot read properties of undefined (reading 'map')
    at OrderList (src/components/OrderList.jsx:24:31)

Getting there takes two steps. First, tell your bundler to generate the maps (in Vite that’s build.sourcemap; every bundler and framework has the equivalent switch). Second, upload them to the tracker during your build. Sentry ships bundler plugins (@sentry/vite-plugin, @sentry/webpack-plugin) and a setup wizard (npx @sentry/wizard) that wire both ends for you. The maps go privately to the tracker; you don’t have to serve them publicly, so you’re not handing your original source to anyone who opens dev tools.

A thousand events, one issue

Here’s the fear people have before turning a tracker on: “my app is going to generate ten thousand errors and I’ll drown.” The dashboard is built around the fix for that. Every individual crash is an event, but the tracker fingerprints events by the shape of their stack trace and groups matching ones into a single issue. A thousand users hitting the same broken line produces one row with a counter, not a thousand rows.

So your inbox is a short list of distinct problems, each with two numbers: total events and users affected. Triage by users affected, always. Raw event count lies to you. An issue with 12,000 events can be one person’s abandoned tab retrying a request in a loop all night. An issue with 60 events and 58 users on your payment route is an actual fire. Same dashboard, opposite priorities, and only the users number tells them apart.

Issues also have a lifecycle. When you fix one, you mark it resolved, and if the same fingerprint shows up again in a later release the tracker reopens it and flags it as a regression. That’s a quiet superpower: you find out your fix didn’t hold from the dashboard, instead of from a second round of silent, swearing users.

The unglamorous workflow

Nobody sits watching an error dashboard all day, and you shouldn’t. The workflow that works is small and a little boring:

  1. Ship a release.
  2. Glance at the tracker over the next day. The default email or Slack alert on “new issue” does most of the watching for you.
  3. Sort by users affected. Fix the top one or two issues that are genuinely yours.
  4. Ignore the noise, then filter it so it stops appearing at all.

The noise is real and you should expect it, because the browser is a hostile place to run code. Extensions inject scripts into your page, and when those scripts crash, your tracker catches them. Ad blockers kill your network requests mid-flight. Cross-origin scripts that error without CORS headers report a bare Script error. with no detail at all. And the all-time classic, the ResizeObserver loop warning, is a benign browser quirk that has burned thousands of collective developer hours despite breaking nothing. Browsers have even reworded it over the years (current Chrome and Firefox say ResizeObserver loop completed with undelivered notifications), which is why the snippet below filters by prefix instead of one exact message.

None of those are your bugs, and the SDK has knobs to silence them at the source:

Sentry.init({
  dsn: "https://[email protected]/0",
  ignoreErrors: [
    /^ResizeObserver loop/, // catches the old and new wordings
    "Non-Error promise rejection captured",
  ],
  denyUrls: [/^chrome-extension:\/\//i, /^moz-extension:\/\//i],
});

The tracker tells you what, the logs tell you why

Error tracking does not replace logging, and the confusion between them trips up almost everyone setting up their first production app. They answer different questions.

The tracker tells you what broke: which line, which release, how often, how many users. It’s aggregated and ranked, built for the question “what should I fix first?” Logs tell you what the system was doing around the failure: the requests that came in before it, the values in play, the database call that timed out 200 milliseconds earlier. They’re sequential and detailed, built for the question “how did we get into this state?”

In practice they hand off to each other. You start in the tracker because it ranks the problems. Then you take the timestamp (and a request ID, if you log one) into your logs to reconstruct the scene. Tracker SDKs even carry a miniature version of this built in: breadcrumbs, a short trail of recent console output, clicks, and network calls attached to each event. Breadcrumbs are often enough for frontend bugs, but for anything involving your server, the real logs are where the story is.

So the next thing you deploy, wire the tracker in before you share the link anywhere. Concretely: create the free project, drop the DSN into your init call, throw the test error, watch it land, delete the debug route, ship. It’s under an hour, and the first week of a launch is precisely when the bugs you can’t reproduce show up. Then, when someone’s checkout dies on a phone you’ve never owned, on a network you can’t simulate, you get a decoded stack trace pointing at line 24, instead of a text from a friend two days later saying something seemed broken once.

signal check

Can you answer these without scrolling back up?

  1. Your tracker shows one issue with 9,000 events and another with 40. Which do you look at first, and which number tells you?

    Sort by users affected, not raw event count. 9,000 events can be one stuck tab retrying in a loop, while 40 events can be 40 different people hitting a broken checkout. The tracker shows both numbers; the users figure is the one that ranks the work.

  2. A production error points at index-BfK3x2.js:1:48211. What is missing and how do you fix it?

    Source maps. The build minified your code, so the stack trace points into the squashed bundle. Generate source maps during the build and upload them to the tracker (the bundler plugins handle both, stamping bundle and map with a matching Debug ID so the tracker can pair them), and the same error decodes to the original file and line.

  3. What does the error tracker tell you that logs do not, and the reverse?

    The tracker tells you what broke, on which line, in which release, how often, and for how many users. Logs tell you what the system was doing around that moment: the requests leading up to it and the state in play. You triage in the tracker, then investigate in the logs.

progress is stored in this browser only