Production bugs test more than technical skill. They test judgment under pressure. A bad bug can trigger panic, rushed patches, and confident guesses that make the problem harder to understand. I have learned that the calmest response is usually the fastest one. Not slow in the sense of passive, but slow in the sense of disciplined. Gather evidence first, reduce noise, and only then decide what to change.
Working remotely from Portmore, I have had to develop a debugging process that works without the luxury of tapping a colleague on the shoulder and live pairing for an hour. My process relies on structured logs, clear evidence, and a systematic narrowing of the search space. This article covers the full workflow I use when something breaks in production.
The first job is stabilizing the situation
When a production issue appears, my first goal is not to prove how fast I can code. My first goal is to understand the blast radius. Who is affected. What is failing. Is the bug total, partial, or environment specific. Can the system be degraded gracefully while I investigate. Those answers change the right next move.
Stabilization might mean hiding a broken path, rolling traffic away from a failing feature, or giving users a clearer fallback. Even a small containment step can buy the time needed to solve the real issue properly. I have seen situations where a five minute feature flag toggle gave the team two hours of calm investigation time instead of two hours of panic.
Questions I ask immediately: Is this affecting all users or a subset? Did a deploy happen recently? Is this a new path or an existing one that regressed? Is there a pattern in the affected accounts, regions, or device types? Those answers shape whether I need a rollback, a hotfix, or more investigation time.
Evidence beats intuition
I do not trust my first theory just because it sounds plausible. I gather logs, reproduce when possible, compare expected versus actual inputs, and look for recent changes that intersect with the failure. If multiple systems are involved, I map the handoff points where assumptions may be breaking down.
The key is to avoid forming a dramatic story too early. Real bugs often come from smaller, less glamorous causes than people expect. A missing field, a stale environment value, a shape mismatch, an unhandled null, a race condition, a cache edge. Evidence keeps the investigation honest.
I also check the git log for recent merges around the area that broke. In my experience, a large percentage of production bugs are connected to something that changed in the last few deploys. Running git log --oneline --since="3 days ago" and scanning for relevant file paths is often the fastest way to narrow things down.
Structured logging setup
Structured logging is one of the highest leverage investments for production debugging. Instead of scattered console.log calls, I use a logger that outputs JSON with consistent fields, making it possible to search, filter, and correlate events across requests.
Here is a minimal structured logging setup I use in Node.js projects:
const pino = require('pino');
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
formatters: {
level: (label) => ({ level: label })
},
timestamp: pino.stdTimeFunctions.isoTime,
base: {
service: 'my-api',
env: process.env.NODE_ENV
}
});
function requestLogger(req, res, next) {
const start = Date.now();
res.on('finish', () => {
logger.info({
method: req.method,
url: req.originalUrl,
status: res.statusCode,
duration: Date.now() - start,
userId: req.user?.id,
requestId: req.headers['x-request-id']
});
});
next();
}
module.exports = { logger, requestLogger };The key fields are requestId for correlating all logs in a single request lifecycle, userId for filtering by affected user, duration for spotting slow requests, and service for identifying which service generated the log in a multi-service setup.
When a production issue hits, I can search for the affected user's ID, filter by error level, and trace the exact sequence of events that led to the failure. Without structured logging, I would be grepping through unstructured text hoping to find something useful.
Source maps in production
Minified stack traces are almost useless for debugging. Source maps let you see the original file names, line numbers, and variable names even when the production code is minified and bundled.
In a webpack configuration, I generate source maps but keep them private:
module.exports = {
devtool: 'hidden-source-map',
output: {
filename: '[name].[contenthash].js'
}
};The hidden-source-map option generates the .map files but does not add the //# sourceMappingURL comment to the bundle, so browsers will not automatically fetch them. You upload the maps to your error tracking service instead.
For Next.js, source maps are configured in next.config.js:
module.exports = {
productionBrowserSourceMaps: false,
webpack: (config, { isServer }) => {
if (!isServer) {
config.devtool = 'hidden-source-map';
}
return config;
}
};This gives your error tracking tool the ability to show you original stack traces while keeping the maps out of public access.
How I narrow the search space
Reproduce the simplest failing case
If I can reproduce the issue, I try to reduce it to the smallest reliable case. That strips away noise. Once the bug becomes smaller, cause and effect are easier to see. I want to know exactly which input, route, state transition, or environment detail makes the problem appear.
That is often the turning point from confusion to clarity. A complex bug that only appears in production with certain user data becomes manageable once you can reproduce it locally with a specific input. I often create a minimal test script or API call that triggers the failure in isolation.
Compare working paths with broken ones
When reproduction is inconsistent, I compare a working case with a failing one and look for the first meaningful divergence. Maybe one user role has a field the other lacks. Maybe one origin sends credentials. Maybe one deploy path included a config file the other did not. Differential debugging is powerful because it forces specific comparison instead of broad speculation.
I like to create two log traces, one for a working request and one for a failing request, and diff them. The first point of divergence is usually very close to the root cause.
Error boundaries in React
React error boundaries prevent a single component crash from taking down the entire page. I add them around major sections of the UI so that a failure in one area shows a fallback instead of a white screen:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
logger.error({
message: 'React error boundary caught',
error: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack
});
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h3>Something went wrong</h3>
<p>We have been notified and are looking into it.</p>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}I wrap this around route level components and around any component that renders user generated or API driven content. The componentDidCatch method sends the error to the logging service with the component stack, which tells me exactly which component tree the error occurred in.
Error tracking and alerting
Setting up error tracking early in a project saves enormous time when things break in production. The setup is usually straightforward:
import * as Sentry from '@sentry/node';
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 0.1,
beforeSend(event) {
if (event.request?.headers) {
delete event.request.headers['authorization'];
delete event.request.headers['cookie'];
}
return event;
}
});The beforeSend hook strips sensitive headers before the error is transmitted. The tracesSampleRate of 0.1 sends performance traces for only 10 percent of requests, which keeps costs manageable while still giving you visibility into slow endpoints.
The real value of error tracking is not just catching errors. It is grouping them, showing frequency trends, and connecting errors to specific releases. When I deploy a new version and see a spike in a specific error group, I know exactly which deploy introduced it.
Debugging without reproduction steps
Some bugs cannot be reproduced on demand. They depend on specific timing, data combinations, or infrastructure state that is hard to recreate. When I cannot reproduce a bug, I focus on:
- Adding more logging around the suspected area so the next occurrence gives me better data
- Reviewing the error tracking tool for patterns: does it happen at a specific time of day, for specific user accounts, or after a certain sequence of actions?
- Checking infrastructure logs for resource exhaustion, connection timeouts, or deployment artifacts that coincide with the error window
- Looking at database query logs for slow queries, deadlocks, or connection pool exhaustion that could cause intermittent failures
I treat unreproducible bugs as opportunities to improve observability. If the bug happens again and I still cannot explain it, the logging was not detailed enough. Each occurrence should leave behind enough evidence to narrow the search further.
Debugging memory leaks
Memory leaks in Node.js usually show up as gradually increasing memory usage that eventually causes the process to crash or become unresponsive. My approach to diagnosing them:
First, I confirm there is actually a leak by monitoring memory over time. A slow upward trend in heap usage that does not flatten after garbage collection is a leak. I use process.memoryUsage() logged at regular intervals to track this.
Common causes I have found in production Node.js applications:
- Event listeners that are added but never removed, especially in long lived server processes
- Closures that capture large objects and keep them alive longer than intended
- Growing arrays or maps used as in-memory caches without eviction logic
- Unclosed database connections or streams
For frontend React applications, common leak sources are subscriptions set up in useEffect without cleanup functions, interval timers that are never cleared, and WebSocket connections that persist after the component unmounts.
The Chrome DevTools heap snapshot comparison is the most effective tool for tracking down the specific objects that are accumulating. Take a snapshot, perform the suspected leaking action several times, take another snapshot, and compare retained object counts.
Why rushed fixes can cost more than the bug
A patch written under panic can hide the original issue, create a second issue, or erase the trail you needed to confirm the real cause. That is why I try to separate a short term mitigation from the proper fix. If a temporary guard clause protects users while I verify the underlying cause, great. But I do not want a temporary bandage to become permanent accidental architecture.
The calmer I stay, the less likely I am to widen the problem. I have seen rushed fixes that introduced worse bugs than the original, because the developer was so focused on making the error message go away that they did not verify the fix actually solved the root cause.
The post mortem process
After resolving a significant production incident, I write a short post mortem that covers:
- Timeline: When the issue was detected, when stabilization happened, when the root cause was identified, when the fix was deployed
- Root cause: What specifically went wrong, not just symptoms but the underlying mechanism
- Detection: How the issue was discovered. Was it alerting, user reports, or manual observation?
- Resolution: What was changed and how it was verified
- Prevention: What guardrail, test, validation, or process change would reduce the chance of recurrence
The post mortem is not about blame. It is about making the system and the team's process stronger. Even small incidents can reveal gaps in monitoring, testing, or deployment practices that are worth addressing before a bigger incident exploits the same gap.
What I learn after the fix ships
A production bug is not fully resolved when the error disappears. I also want to know why the issue reached production, what signal could have revealed it earlier, and what lightweight guardrail would reduce the chance of recurrence. That might mean better validation, better logging, clearer contracts, or improved rollout discipline.
The point is not blame. The point is leverage. Every incident can make the system and the process a little stronger if you extract the right lesson. I keep a running document of lessons learned from past incidents, and I review it before major deploys.
Calm is a technical skill
People sometimes treat calmness like a personality trait. I see it more as a debugging skill. A calm process protects accuracy. It helps you separate symptoms from causes, observe the system more clearly, and avoid performing theater while users are waiting for a real solution.
If your team needs help improving the product and the process around it, look at my maintenance service, review the security fundamentals article, or hire me for product work that values sharp diagnosis as much as clean implementation.