The Debugging Dojo: A Senior's Guide to Turning Junior Devs into Bug Detectives
Your Most Expensive Resource Isn't Your Cloud Bill
How much of your week gets eaten by Slack messages? A junior developer sends a screenshot of a cryptic error, followed by "Can you hop on a quick call?" That call becomes a 45-minute debugging session where you pilot their machine, find the null pointer they missed, and spend another 15 minutes explaining why it happened.
Your most expensive resource isn't Kubernetes or Lambda invocations. It's senior engineer attention. Every time it's fractured to solve a problem someone else could have handled, the whole team slows down.
We kept hiring talented junior developers, throwing them into a complex codebase, and hoping they'd figure it out. After one too many Friday afternoon hotfixes, we decided hope wasn't a viable engineering practice. We needed a system.
This is how we stopped being the team's human debugger and started building methodical problem-solvers.
The panic-driven console.log cycle
A junior developer facing a bug usually follows this pattern:
- Something breaks. Panic sets in. The instinct is to do something, anything.
- Scatter
console.log('here 1'),console.log(variable),console.log('WTF')throughout the code. - Re-run, squint at a wall of unstructured log output, try to parse the state of the universe.
- After 30 minutes, confidence is gone. They conclude it's the framework's fault and tap a senior on the shoulder.
This isn't a knock on juniors. It's a symptom of not having a framework for investigation. They treat debugging as dark magic when it should be a repeatable process.
The Debugging Dojo
We built a structured practice to replace panic with method.
The scientific method for code
Before anyone touches a debugger, we run a workshop on the core principles:
Observe, don't assume. What is actually happening? Not what you think is happening. Read the error message. All of it. Read the full stack trace. Check the logs for the exact request ID. The answer is usually right there if you have the patience to read.
Make a testable hypothesis. "The code is broken" is not a hypothesis. "The userId is undefined because the JWT payload doesn't include it" is a hypothesis. You can immediately design an experiment to confirm or disprove it.
Design the smallest experiment. Don't rewrite the function. Can you add one log? Set one breakpoint? Write one unit test that isolates this exact condition?
Iterate. Your hypothesis was wrong? Good. You eliminated a possibility. That's progress.
The bug zoo
We built a bug-zoo repository: a small NestJS app with curated bugs from our own production incidents (names changed). Each bug is a self-contained lesson:
- Off-by-one: Classic loop or slice error.
- Async ghost: Race condition from a misunderstood
Promise.all. - Mutable state: A function that accidentally mutates an object passed by reference.
- Env var mix-up: Works locally, fails in staging.
- DI scope puzzle: NestJS provider set to
Scope.REQUESTwhen it should be a singleton.
During the weekly session, a junior picks a bug, shares their screen, and leads the investigation for an hour. The senior's job is not to give the answer but to ask questions:
Junior: "I'm stuck. I don't know where to look." Senior: "What do you know for sure? What's the expected output? The actual output? Walk me through the stack trace. What's the last line of our code that ran?"
Uncomfortable silences happen. But they force the junior to articulate their thought process, which almost always reveals the flawed assumption.
Graduation: the post-mortem
Solving the bug is half the job. To graduate, you write a short, blameless post-mortem:
- Summary: One sentence describing the problem.
- Root cause: Not "the variable was null" but "the data transformation layer didn't handle empty
itemsarrays from the external API, passingnullto the pricing service." - Investigation path: Which hypotheses were wrong? What was the 'aha' moment?
- Prevention: What stops this class of bug from recurring? New ESLint rule? Zod runtime parsing? Stricter TypeScript config?
This turns a single bug fix into institutional knowledge.
Walkthrough: the phantom discount
From the bug zoo. A function calculateFinalPrice is intermittently applying a discount twice.
interface Cart {
items: { price: number }[];
discountCode?: string;
}
function applyDiscount(cart: Cart, discountPercentage: number): Cart {
cart.items.forEach(item => {
item.price = item.price * (1 - discountPercentage / 100);
});
return cart;
}
export function calculateFinalPrice(cart: Cart): number {
let finalCart = cart; // This is a reference, not a copy!
if (cart.discountCode) {
const discount = getDiscountFromDb(cart.discountCode);
finalCart = applyDiscount(finalCart, discount);
}
if (isFirstTimeCustomer(cart.customerId)) {
finalCart = applyDiscount(finalCart, 5);
}
const total = finalCart.items.reduce((sum, item) => sum + item.price, 0);
return parseFloat(total.toFixed(2));
}Investigation:
- Hypothesis:
getDiscountFromDbreturns the wrong value. Add a log. Returns 10. Hypothesis false. - Hypothesis:
applyDiscountis called in the wrong order. Step through with a debugger. Watch as the first call modifiesitem.price. See the same object with the already-reduced price enter the secondapplyDiscountcall.
Root cause: applyDiscount mutates its input argument. finalCart is a reference to the original cart, so both discount applications modify the same data.
Fix: Make it a pure function:
function applyDiscount(cart: Cart, discountPercentage: number): Cart {
const newItems = cart.items.map(item => ({
...item,
price: item.price * (1 - discountPercentage / 100),
}));
return { ...cart, items: newItems };
}Systemic fix: Team convention against object mutation in utility functions. ESLint rule to enforce it.
The payoff
It took time. It required consistent effort from senior staff to teach instead of fix. But the results were clear:
- "Quick questions" plummeted. When they did come, they were specific and well-researched.
- Junior developers started treating bugs as puzzles, not personal failures.
- Root cause analysis led to systemic improvements. We stopped fixing individual bugs and started eliminating entire categories of them.
Stop renting your senior engineers' time to fight fires. Invest it in building a team of firefighters.