What error tracking covers
Error tracking does one job well. It records uncaught exceptions and unhandled promise rejections with a stack trace, the browser and the release. Many setups also record failed requests with their status code, and crashes where a page stops rendering. If your code throws, you will know where and how often.
All of these start with a signal: an exception, a rejection, a 500. Many front-end bugs give no signal. Every request returns 200, every handler finishes, and the screen is still wrong. The user sees the problem. The tracker has nothing to record.
Four bugs that raise no error
A duplicate submit. A user clicks "Place order" once. The handler is attached twice, or a re-render fires the submit again, and two POST requests go out. Both return 201, and the customer has two orders. See how to prevent duplicate submissions.
A stale response shown over newer data. A user types "pa", then "paris". The request for "pa" is slower and lands last, so the list shows results for "pa" under a box that says "paris". Both responses were valid. This is the bug behind out-of-order search results and most race conditions in React data fetching.
A lost edit. A notes field saves as the user types. Two saves are in flight, and the older one reaches the server last. The server keeps the older text, and the user's last sentence is gone after a reload.
A request that keeps failing quietly. A call to /api/notifications returns 500 every time. The code retries, gives up and renders "No notifications". The failure was caught on purpose, so nothing is thrown, and the empty list looks normal.
Visual breakage is in the same family. Text that renders white on a white background, or a "Pay" button covered by another element, stops the user as surely as a crash. No request failed and no code threw.
What a monitor has to watch
These bugs share one thing: each event on its own is normal. A click is fine. A 200 is fine. A state write is fine. The bug is in how they line up, so a monitor has to record three kinds of event and the order between them.
| Signal | What it tells you |
|---|---|
| User actions: clicks, submits, typing | What the user asked for, and how many times. |
| Each request: method, path, start time | What the app sent for each action, and whether one action sent two requests. |
| When each response was delivered | Whether responses came back in the order the requests were sent. |
| What state each response wrote | Which response the screen is showing now. |
| The order of all three | Whether an older response overwrote a newer one, or one action caused two writes. |
None of this needs page content. Order, timing and counts are enough.
Level 1: record request order yourself
You can start today with a small wrapper around fetch. Give each request a key that names what its response will write, such as "search" or "note:42". The wrapper numbers the requests per key and reports when an older response is delivered after a newer one.
const started = new Map(); // key -> number of the newest request sent
const delivered = new Map(); // key -> number of the newest response delivered
export async function trackedFetch(key, url, options) {
const seq = (started.get(key) ?? 0) + 1;
started.set(key, seq);
const response = await fetch(url, options);
const newest = delivered.get(key) ?? 0;
if (seq < newest) {
report("stale-response", { key, seq, newest }); // an older response landed last
} else {
delivered.set(key, seq);
}
return response;
}
function report(name, data) {
console.warn(`[monitor] ${name}`, data);
}
// usage
const res = await trackedFetch("search", `/api/search?q=${encodeURIComponent(q)}`);
The wrapper only reports. The caller still gets the response, so the bug is still there, but now you know which key it happens on and how often. The same idea covers duplicates: count writes with the same method, path and body that start within a second or two of each other.
Level 2: send custom events to your logs
A console warning helps on your machine. In production, change report to send each event to the logs or error tracker you already use, as a custom event. Then it sits next to your exceptions and you can alert on it.
report("duplicate-submit", {
route: "POST /api/orders", // method and path, no query string
count: 2, // requests from one click
gapMs: 180, // time between them
});
Send: the event name, the method and path, the status code, sequence numbers, timings in milliseconds, counts and your release version.
Do not send: page text, form values, request or response bodies, query strings, names, emails or tokens. If a path holds an ID, replace it with a placeholder such as /api/notes/:id before sending.
Count quiet failures the same way. Report when a route fails several times in a row, even if a retry or an empty state hid it from the user.
Level 3: a dedicated runtime monitor
A wrapper sees only the calls that go through it. It misses requests made by libraries, and it cannot see which response wrote which piece of state. A dedicated runtime monitor is worth it when:
- The app has many fetches written by different people, and you cannot wrap them all.
- Responses write into a shared store, so the stale write happens far from the request.
- You need the user action, the request, the response and the state write in one report, without adding code at every call site.
Checklist
- Keep error tracking for exceptions, failed requests and crashes.
- Number requests per key, and report an older response that lands after a newer one.
- Count repeated writes from one user action.
- Report routes that keep failing, even when a retry or an empty state hides it.
- Send order, timing and counts. Never send page text or personal data.
Where GenClass fits
GenClass is an open-source browser runtime (Apache-2.0) that watches requests and state writes, and uses a small on-device model to report stale responses, duplicate submits and failing endpoints with the evidence. Its default mode only reports and changes nothing. Reports can be sent on to your own logging. Set it up with one command, npx genclass, then see how GenClass works and the benchmarks.
Related: race conditions in React, out-of-order responses and preventing duplicate form submissions.