Debugging JavaScript in 2026: A Practical Workflow for Real Bugs

Most JavaScript bugs come down to looking at the wrong thing at the wrong time. I debug in order from cheapest to most involved: labeled console logs first, then breakpoints in DevTools, then the debugger statement, then network and source maps. Confirm what the code actually does before you guess why.
Part of the complete web design guide.
On this page
- Start with the console, but use it properly
- Learn the Sources panel, not just the Network tab
- Conditional and logpoint breakpoints
- Source maps are the difference between debugging and guessing
- Handle async bugs differently
- Track errors in production, because you can’t debug what you can’t see
- A quick note on tooling
- FAQ
- Why does console.log show a different value than what I logged?
- My breakpoints land on the wrong line. What’s wrong?
- Do I really need error tracking for a small site?
Most JavaScript bugs aren’t mysterious. They’re just invisible until you look at the right thing at the right time. Over the years I’ve built a repeatable process for tracking them down, and it works whether you’re debugging a form on a local business site or a 200-file React app.
Here’s how I actually approach it, in order, from cheapest to most involved.
Start with the console, but use it properly
console.log() gets mocked, but the problem is usually how people use it, not the tool. Logging twelve things in a row tells you nothing. Logging a labeled object tells you a lot.
// Bad: you'll never know which log is which
console.log(data);
console.log(data.items);
console.log(data.items[0].price);
// Better: label it and log the whole shape once
console.log('cart payload', data);
If you want to see structure without clicking through a tree, use console.table() for arrays of objects and console.dir() when you need to inspect a DOM node’s actual properties instead of its rendered HTML.
console.table(orders, ['id', 'total', 'status']);
// Group related output so it collapses in the console
console.group('checkout');
console.log('subtotal', subtotal);
console.log('tax', tax);
console.groupEnd();
Two things people forget. First, console.log holds a live reference to objects in most browsers, so if the object mutates later, what you see when you expand it may not be what it was when you logged it. Wrap it in JSON.parse(JSON.stringify(obj)) if you need a frozen snapshot. Second, debugger; in your source does the same thing as clicking a breakpoint, and it survives a page reload. I drop those in, reload, and let the browser stop exactly where I need it.
Learn the Sources panel, not just the Network tab
Almost every “the data is wrong” bug comes down to one of two things: the request never sent what you thought, or the response wasn’t what you assumed. The Network tab answers both, but only if you read it correctly.
- Click the request, then the Payload tab. That’s what actually left the browser. Not what your code intended.
- Check the Response tab, not the console. A 200 status with an HTML error page inside is extremely common.
- Right-click a request and choose Copy as fetch to reproduce it in the console or in a test.
- Use the Initiator column to find which line of code fired the request when you have no idea where it’s coming from.
If the request is fine and the response is fine, the bug is in your code between them. That’s when you move to breakpoints.
Conditional and logpoint breakpoints
Clicking a line number stops every time that line runs. In a loop over 10,000 records, that’s useless. Right-click the line number instead:
- Add conditional breakpoint and type an expression like
item.id === 4821. It stops only then. - Add logpoint and type a message like
processing {item.id}. It logs without pausing, and without editing your source.
Logpoints are the single biggest quality-of-life upgrade in browser devtools in the last several years. You get the visibility of a log statement with none of the cleanup or redeploy.
Source maps are the difference between debugging and guessing
If you’re shipping bundled, minified, or TypeScript code and your breakpoints land in the wrong place, your source maps are broken or missing. In 2026 most build tools do this well by default, but the common failure is a mismatch between where the map file is served and where the bundle says it lives.
// vite.config.js
export default {
build: {
sourcemap: true, // 'hidden' for production if you don't want to expose it publicly
},
};
For production, I usually set sourcemap: 'hidden'. The map gets generated and uploaded to your error tracker (Sentry, Bugsnag, Rollbar, whatever you use), but the browser isn’t told about it, so end users can’t pull your original source. You still get readable stack traces in your dashboard.
One gotcha that eats hours: if your error tracker shows app.4f2a1c.js:1:88234 instead of a real function name, the map didn’t upload or the release version didn’t match. Fix the release tag before you debug anything else.
Handle async bugs differently
Async bugs are where most people get stuck, because the stack trace points somewhere unhelpful and the state you care about is gone by the time you look. Two habits help.
First, always await or return your promises. A missing await doesn’t throw, it just silently does nothing useful, and the error shows up somewhere unrelated later.
// The bug: no await, so errors vanish and order breaks
async function saveAll(items) {
items.forEach((item) => saveItem(item));
}
// Fixed: sequential when order matters
async function saveAll(items) {
for (const item of items) {
await saveItem(item);
}
}
// Fixed: parallel when order doesn't matter, but still awaited
async function saveAll(items) {
await Promise.all(items.map((item) => saveItem(item)));
}
Second, use Promise.allSettled instead of Promise.all when you need to know which items failed rather than just that something failed. Promise.all rejects on the first failure and throws away the results of everything that succeeded, which is rarely what you want in a batch operation.
For unhandled rejections, add a listener during development so nothing disappears silently:
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
});
Track errors in production, because you can’t debug what you can’t see
Local debugging only covers bugs you can reproduce. Real users hit the ones you can’t. A minimal error tracker pays for itself the first week. If you don’t want a third-party service, you can wire up the basics yourself.
window.addEventListener('error', (event) => {
navigator.sendBeacon('/api/log-error', JSON.stringify({
message: event.message,
source: event.filename,
line: event.lineno,
column: event.colno,
stack: event.error?.stack,
url: location.href,
ts: Date.now(),
}));
});
sendBeacon is the right call here because it survives the page unloading. A regular fetch in an error handler often gets cancelled when the user navigates away, which is exactly when the worst errors happen. On the server side, log the user agent and a session ID so you can group reports instead of reading the same error forty times.
A quick note on tooling
Most of my debugging happens in Chrome or Firefox devtools, but if you write a lot of JavaScript, a few other things earn their keep. TypeScript catches a large class of “this was undefined” bugs before they ship. ESLint with the promise and no-floating-promises rules catches the missing await problem automatically. And for anything involving state, the React DevTools or your framework’s equivalent will show you the actual value at render time instead of making you guess.
None of this is exotic. It’s just a matter of checking the request before the code, the data before the logic, and the production error feed before assuming you know what’s broken. If you’d rather hand the maintenance off entirely, this is the kind of work that falls under Long Island web design and ongoing site care, and it’s most of what I do on a given week.
FAQ
Why does console.log show a different value than what I logged?
Because the console keeps a live reference to objects. If the object changes after you log it, expanding it later shows the new state. Log a deep copy with JSON.parse(JSON.stringify(obj)) when you need a true snapshot.
My breakpoints land on the wrong line. What’s wrong?
Usually a source map problem. Either the map isn’t being generated, isn’t being served at the path the bundle expects, or your build tool is serving a cached version. Check the Sources panel for your original file names. If you only see minified code, the map isn’t loading.
Do I really need error tracking for a small site?
If real people use it and you can’t watch it constantly, yes. Even a basic handler that beacons errors to your own endpoint is enough to stop flying blind. The alternative is hearing about bugs from customers, which is worse.


