Node.js
Track exceptions raised on your Node backend. Server errors land in the same Nohmo project as your frontend errors, so a 500 in your API sits next to the JS error it caused in the browser — same Errors page, same grouping, same daily digest.
Ships inside the nohmo package you already install, with no runtime dependencies — Node built-ins only. It never imports React or anything browser-side, because an error reporter should not be the thing that breaks the app it is watching.
Install
npm install nohmo
The server code lives on its own entry point, nohmo/server, so nothing server-side can end up in a browser bundle.
Express
Register the error handler last, after every route and router. Express only shows an error handler what was registered before it — this is the opposite of the Django middleware, which goes first.
const express = require('express')const { init, expressErrorHandler } = require('nohmo/server')init({projectId: process.env.NOHMO_PROJECT_ID,apiKey: process.env.NOHMO_API_KEY,environment: process.env.NODE_ENV, // optionalrelease: process.env.GIT_SHA, // optional - ties errors to a deploy})const app = express()app.get('/orders/:id', getOrder)// ... all your routes ...app.use(expressErrorHandler()) // LAST
It reports the error and then passes it straight on, so your own error page or JSON response is unchanged. To skip errors that are expected traffic rather than defects:
app.use(expressErrorHandler({shouldReport: (err) => err.status !== 404,}))
Any other framework
Wrap a node:httphandler. This covers Connect, Koa's raw layer, a Next.js custom server, or a hand-rolled server.
const http = require('node:http')const { init, wrapHandler } = require('nohmo/server')init({ projectId: '...', apiKey: process.env.NOHMO_API_KEY })http.createServer(wrapHandler(async (req, res) => {// anything thrown here is reported, then rethrown})).listen(3000)
Reporting by hand
const { captureException, captureMessage } = require('nohmo/server')try {await charge(order)} catch (err) {captureException(err, {request: { path: '/checkout' },extra: { orderId: order.id },})}captureMessage('nightly reconciliation finished with 3 mismatches')
Short-lived processes
Events ship on a timer that is deliberately unref'd, so it can never hold your process open — which also means it may not fire on the way out. A cron job, a Lambda or a one-off script should flush explicitly.
const { flush } = require('nohmo/server')const ok = await flush(5000)// false means events were dropped - worth checking in a job you care about
Options
| Option | Default | What it does |
|---|---|---|
| environment | 'production' | Tags every event |
| release | '' | Ties errors to a deploy |
| serverName | os.hostname() | Groups errors per instance |
| sampleRate | 1 | Fraction of errors sent |
| dedupWindow | 5 | Seconds before an identical error is sent again |
| queueSize | 1000 | Bounded - drops rather than growing without limit |
| batchSize | 50 | Events per request |
| flushInterval | 5 | Seconds before a partial batch ships |
| sendDefaultPii | false | Include headers, query strings and user email |
| endpoint | https://www.nohmo.in/... | Override the ingest URL — only for self-hosted or tests |
| debug | false | Verbose logging |
Privacy
sendDefaultPii is off by default: no headers, no query strings and no user email leave your server. Turn it on and everything is still run through a credential scrubber that redacts Authorization, Cookie, Set-Cookie, X-API-Key and any key containing secret, password, token, api_key, private_key or credential — at any nesting depth.
userIdis always sent, since an id is not PII on its own. The user's email is sent only with sendDefaultPii: true.
Behaviour under load
The queue is bounded and drops the oldest events when full. A crash loop generates errors faster than any network can ship them, and an unbounded queue there is a memory leak that ends in an OOM kill — the SDK becoming the outage. Identical errors are deduplicated within dedupWindow, failed sends retry with exponential backoff and jitter, and a Retry-After from the server is honoured.
If the ingest host cannot be reached at all — blocked egress, a TLS failure, bad DNS — the SDK warns once, and once again on recovery. Silent failure is the one thing an error reporter must never do.
nohmo: cannot reach https://www.nohmo.in/api/tracker/track/ (...) - events are being dropped.
Note: Nothing the SDK does can throw into your request path. captureException swallows its own failures, and flush() resolves false rather than rejecting.
Grouping
Errors are grouped by type, message and the first stack frame that names your code. V8 puts built-ins at the top of a Node stack — at JSON.parse (<anonymous>) — so Nohmo skips those and groups on the real call site instead. Two endpoints failing inside the same built-in stay separate issues.