Debugging and Systematic Troubleshooting Questions
Diagnosing defects methodically: reproducing failures, forming and testing hypotheses, reading stack traces and logs, bisecting changes, and reasoning about error handling and edge cases. Covers a disciplined root-cause approach that applies from local bugs to production issues, distinct from embedded hardware-level debugging. A universally probed engineering-craft skill.
Given a function that sometimes throws an exception deep inside a library you cannot modify, explain how you would instrument the codebase to capture a stack trace and the relevant contextual variables without changing the library's own code. Describe approaches for at least two of Python, Java, or Node.js.
Sample Answer
Direct answer
You can capture a stack trace and the relevant local state without touching the library's source by instrumenting at the BOUNDARY where you call into it: wrap the call site in a try/catch (or its language equivalent) that re-raises after logging, install a global/uncaught-exception hook that fires regardless of where the exception originates, or attach a debugger/tracer that breaks on the exception type without modifying any code at all.
Structured elaboration
Three complementary approaches, from least to most invasive:
- Wrap the call site, not the library. Since you control the code that CALLS into the library, even if you can't modify the library itself, catching the exception at your own call site and logging the full stack trace, the exception type/message, and any locally-relevant variables (the arguments you passed in, the current request context) before re-raising gives you a permanent, low-overhead capture point. In Python:
try: library_call(...) except Exception: logger.exception("context: %s", relevant_vars); raise. In Java: a try/catch around the call that logs viae.printStackTrace()or a structured logger before rethrowing. In Node.js: wrapping in a try/catch for synchronous calls, or.catch()on the returned Promise for async ones. - Install a global uncaught-exception/unhandled-rejection hook. This catches cases where the exception surfaces somewhere you didn't anticipate wrapping: Python's
sys.excepthook, Java'sThread.setDefaultUncaughtExceptionHandler, Node'sprocess.on('uncaughtException', ...)andprocess.on('unhandledRejection', ...). These fire regardless of exactly where inside the library the exception originated, at the cost of being a last-resort catch-all rather than a precisely-scoped one. - Attach a debugger or tracer without changing code, when you need MORE than a stack trace, such as full local variable state at the moment of the throw. Most debuggers support "break on exception" (Python's
pdbwithpython -m pdb, orimport pdb; pdb.set_trace()combined with a signal, or a conditional breakpoint set on the exception type in a full IDE debugger; Java's debugger supports exception breakpoints directly in most IDEs) which pauses execution at the exact throw site, inside the library's own code, letting you inspect the full call stack and locals without ever editing the library.
Worked example
A third-party HTTP client library occasionally raises an unlabeled ConnectionError with no useful message, and you need to know what request triggered it. Wrapping the call site: try: response = third_party_client.get(url, timeout=5) except ConnectionError: logger.exception("third_party_client failed for url=%s, timeout=%s", url, 5); raise. This doesn't change a single line of the library, but every future occurrence now logs the exact URL and timeout that triggered it, alongside the library's own stack trace, immediately giving you the context needed to distinguish "this URL is consistently unreachable" from "this only fails under a specific timeout value."
Trade-offs and pitfalls
A global uncaught-exception hook is a safety net, not a substitute for a targeted wrap at the call site: it tells you SOMETHING failed somewhere, but without the call-site context (what arguments were in play, what business operation was in progress) it's often not enough to actually diagnose the issue, only to know it happened. The debugger approach is the most informative but requires either a reproducible local trigger or an interactive session, so it's best reserved for cases where the logged stack trace alone isn't enough to form a hypothesis.
Write a memory-efficient Python function parse_log_counts(file_path, top_n=5) that scans a large server log file, which may be larger than available memory, and returns the top N error types or HTTP status codes by count without loading the whole file into memory. Example lines: 2025-01-01T12:00:00Z INFO request_id=1 status=200 and 2025-01-01T12:00:01Z ERROR request_id=2 status=500 exception=ValueError. Describe edge cases and how your implementation handles gzipped logs and malformed lines.
Sample Answer
Direct answer
Stream the file line by line instead of loading it into memory, maintaining only a bounded Counter of error-type/status-code totals as you go, which keeps memory usage proportional to the number of DISTINCT keys seen rather than the number of lines in the file. Handling gzipped input just means picking the right file-opening function based on the extension; handling malformed lines means skipping them without crashing the whole scan.
Structured elaboration
import gzip
from collections import Counter
from typing import List, Tuple
def parse_log_counts(file_path: str, top_n: int = 5) -> List[Tuple[str, int]]:
counts = Counter()
opener = gzip.open if file_path.endswith('.gz') else open
with opener(file_path, 'rt', errors='replace') as f:
for line in f:
line = line.rstrip('\n')
if not line:
continue
status, exc = None, None
for tok in line.split():
if tok.startswith('status='):
status = tok.split('=', 1)[1]
elif tok.startswith('exception='):
exc = tok.split('=', 1)[1]
if exc:
counts[f'exception:{exc}'] += 1
elif status:
counts[f'status:{status}'] += 1
# lines with neither field are silently skipped as malformed
return counts.most_common(top_n)
Key design points:
- Iterating
for line in freads one line at a time from the underlying file object rather than materializing the whole file in memory; this is what makes the function safe against a 10GB+ input. gzip.openversusopenare chosen based on the file extension, so the same function transparently handles both compressed and uncompressed logs without the caller needing to know which.errors='replace'on the text-mode open prevents a single malformed byte sequence (a truncated write, a binary artifact mid-file) from raising aUnicodeDecodeErrorand aborting the entire scan.- Lines with neither a
status=norexception=token are simply skipped rather than raising, satisfying "handle malformed lines" without crashing on them. Counter.most_common(top_n)does the top-N selection in O(k log n) where k is the number requested, more efficient than sorting the entire counts dictionary when only a few top entries are needed.
Worked example
A synthetic log with 50 status=200 lines, 12 exception=ValueError lines, 8 exception=TimeoutError lines, 20 status=404 lines, plus two malformed lines (one garbage line, one blank):
plain file: [('status:200', 50), ('status:404', 20), ('exception:ValueError', 12), ('exception:TimeoutError', 8)]
gzipped file: [('status:200', 50), ('status:404', 20), ('exception:ValueError', 12), ('exception:TimeoutError', 8)]
top_n=2: [('status:200', 50), ('status:404', 20)]
The gzipped and plain versions of the identical content produce byte-identical results, confirming the compression branch works correctly, and top_n=2 correctly truncates to the two highest counts without needing a separate code path.
Edge cases
- Empty file: the loop simply never executes,
countsstays empty, andmost_common(top_n)returns an empty list, no special-casing needed. - File with only malformed lines: same result, an empty list, which is the correct behavior (nothing to report) rather than an error.
- A line with BOTH
status=andexception=: the current implementation prioritizesexception:(checked first), which is a deliberate choice since an exception is generally the more specific and actionable signal; this priority should be called out explicitly rather than left as an implicit accident of code order. top_nlarger than the number of distinct keys:most_commonsimply returns all available entries, no error.
Trade-offs and pitfalls
This implementation keeps ALL distinct keys in memory (bounded by the number of distinct error types/status codes, not the number of log lines, which is normally small), but if the log contained a very high-cardinality field mistakenly counted this way (say, counting by full request URL instead of status code), memory could still grow unboundedly with the number of distinct values; the safety guarantee here specifically relies on error types and status codes being a naturally small, bounded set.
Explain rubber-duck debugging and describe how you would use it collaboratively to help a teammate find a bug they are stuck on. Include when it is appropriate to escalate to active pair programming versus continuing to guide them through the technique.
Sample Answer
Direct answer
Rubber-duck debugging is the practice of explaining your code or problem, line by line, out loud, to an inanimate object (or any passive listener), on the theory that the act of ARTICULATING your assumptions forces you to notice the gap between what you believe the code does and what it actually does; used collaboratively, you become the "duck," a real listener whose job is to stay quiet and let the other person talk through it, only interjecting with clarifying questions, not answers.
Structured elaboration
Why it works: most debugging time is lost not to hard problems but to an unexamined assumption ("of course X is true here") that the explainer has never actually had to state out loud; the discipline of narrating each step forces exactly that statement, and the mismatch often becomes obvious the moment it's spoken, even before the listener says anything at all.
How to use it collaboratively to help a stuck teammate:
- Sit with them and ask them to explain the problem from the beginning, including what they expect to happen and what's actually happening, as if you know nothing about the code (even if you do); resist the urge to jump in with your own theory immediately, since the goal is for THEM to find the gap through their own narration, not for you to hand them the answer.
- Ask clarifying, not leading, questions when something seems glossed over: "what does this variable contain at this point?" rather than "isn't this variable actually null here?"; a clarifying question preserves the self-discovery effect, while a leading question short-circuits it, in effect just answering the question yourself, in a slightly slower way.
- Notice when they pause or hesitate on a specific line, since that hesitation is itself a signal, often the exact spot where their explanation doesn't fully hold together, even if THEY haven't consciously noticed it yet; gently returning to that point ("can you say more about what happens right there?") is more effective than moving on.
- Let silence do some of the work. Resisting the urge to fill every pause with your own guess gives them room to keep narrating and often arrive at the insight themselves, which is both faster (no back-and-forth debugging your OWN, potentially wrong, theory) and more valuable for their own learning than being handed the answer.
When to escalate to active pair programming instead of continuing to guide:
- When the explanation reveals a gap in UNDERSTANDING (not just an overlooked line) that narration alone won't resolve, e.g., a genuine misunderstanding of how a language feature or library behaves, where providing the missing knowledge is more useful than more questions.
- When you notice, through their narration, something concrete they clearly haven't seen (a specific line, a specific value) and pointing it out directly is faster and kinder than continuing an extended Socratic process once the value of self-discovery has been exhausted for this specific bug.
- When time pressure genuinely doesn't allow for the (often somewhat slower) self-discovery process, in which case switching to active pairing, working the problem together directly, is the pragmatic choice, with an explicit acknowledgment that you're switching modes for a good reason, not simply losing patience.
Worked example
A teammate stuck on why a function returns stale data: walking them through explaining the code line by line, they narrate "and then this checks the cache... and returns it if it's still valid... I wanted a sixty-second expiry, so I set the value to sixty thousand..." and pause, mid-sentence, on "sixty thousand," suddenly noticing they'd been assuming the codebase's convention was MILLISECONDS (so they multiplied their intended sixty seconds by 1000 before storing it), when the surrounding code that actually reads this field treats it as SECONDS directly, with no conversion; storing 60000 where the code expects 60 means the real expiry is 60000 seconds (about 16.7 hours), roughly 1000x longer than the sixty seconds they intended, which is exactly why the data was going stale. They found this entirely through their own narration; the only input needed was staying quiet and letting them keep talking through it.
Trade-offs and pitfalls
Rubber-duck debugging (with a real, silent listener) works best for a SPECIFIC kind of stuck-ness, an unexamined assumption, not for a genuine knowledge gap or a problem requiring information the person simply doesn't have; recognizing which kind of "stuck" you're looking at, and switching to direct guidance or active pairing when it's the latter, is what keeps the technique from wasting time on a problem it was never suited to solve.
Explain the practical differences between debugging at the application level versus the infrastructure level (network, storage, compute). Give two examples of failures that look similar at first glance but have different root causes at each level, and describe how you would distinguish between them.
Sample Answer
Direct answer
Application-level debugging asks "why did this code produce the wrong result," and its evidence lives inside your process: variables, call stacks, exceptions, business logic. Infrastructure-level debugging asks "why couldn't the correct code even run correctly," and its evidence lives outside your process: network reachability, disk and memory pressure, DNS, container scheduling, resource limits. The two failure modes can look identical from the outside (a request fails, a page times out) while requiring completely different evidence to distinguish.
Structured elaboration
The practical test: if you could run the exact same code on a machine with unlimited, perfectly-behaving resources and a clean network, would the bug still happen? If yes, it's application-level (a logic error, a bad state transition, an unhandled edge case). If no, it's infrastructure-level (the code was never wrong, but its environment failed to give it what it needed).
Two concrete examples that look alike but diverge in root cause:
- "Requests are timing out." Application-level cause: a newly introduced N+1 query pattern (fetching a list, then running one extra database query per item in that list instead of one combined query, so 100 items means 101 round trips instead of 1) makes one endpoint genuinely slow under load, so it exceeds a client timeout. Infrastructure-level cause: the container's memory limit is too tight, so the process is spending most of its time in garbage collection (or worse, getting OOM-killed (the operating system forcibly terminating the process because it used more memory than it was allowed to) and restarted), and the timeout is really a symptom of resource starvation that has nothing to do with the query logic. Both present as "P99 latency spiked" in a dashboard.
- "Intermittent 500 errors on one specific endpoint." Application-level cause: a race condition in that endpoint's handler corrupts shared state under concurrent access. Infrastructure-level cause: that endpoint happens to be the one that calls out to a dependency whose DNS resolution is flaky, so the failures are really network-layer and would happen on ANY endpoint that made the same external call.
How to distinguish them in practice: check infrastructure-level signals FIRST because they're cheap and rule out a whole class of causes at once: container restarts/OOM kills, CPU throttling, disk pressure, DNS/network error rates, host-level resource graphs. If those are clean, the evidence points inward to application logic, and now stack traces, code paths, and business-logic invariants become the productive place to look. Going the other direction (deep-diving application logs before checking whether the host was even healthy) wastes time chasing red herrings in code that was never the problem.
Worked example
Given "checkout intermittently fails for large carts": check host-level memory/CPU graphs for the checkout service during the failure window (infra-level, five minutes). If those are flat and unremarkable, the next cheapest infra check is whether the failures correlate with a specific downstream dependency's error rate (still infra-adjacent, still cheap). Only once both come back clean do you profile the actual cart-processing code path, because now the evidence has ruled out "the environment failed the code" and pointed at "the code itself has a large-cart-specific bug," likely a data-size-dependent logic error, not a data-size-dependent resource limit.
Trade-offs and pitfalls
The trap is assuming the layer based on which tools you personally know best rather than what the symptom actually implies. A backend engineer with no infra background will often burn an hour reading application logs for a problem that a five-second look at a container-restart graph would have explained. The fix is cheap and disciplined: always take the five-minute infra-layer look first, even if you expect it to come back clean, because ruling it out is what makes the deeper application-level dig trustworthy.
Provide a succinct example of a recent small bug you fixed. Include the minimal code before and after in a language of your choice, explain the root cause, why your fix works, how you tested it, and what you learned that will help you avoid similar bugs in the future.
Sample Answer
Direct answer
A recent example: a function computing a running total returned a subtly wrong result specifically when passed an empty input list, because it assumed at least one element would always be present; the root cause was an unchecked assumption about the input's shape, not a logic error in the calculation itself, and the fix was a one-line explicit guard plus a test covering the previously-unconsidered case.
Structured elaboration and worked example
Before:
def average_response_time(durations):
total = sum(durations)
return total / len(durations)
This looks correct and passes any test using a non-empty list. The bug: called with an empty list (a genuinely possible input, e.g., a time window with zero recorded requests), it raises ZeroDivisionError, which in this specific case was crashing a reporting job whenever a low-traffic service had a quiet hour with literally zero requests, a real, recurring production scenario the original code never considered.
After:
def average_response_time(durations):
if not durations:
return None # no data for this window; caller decides how to display that
total = sum(durations)
return total / len(durations)
Root cause: the function was written and tested against realistic-looking sample data, which always happened to be non-empty, so the implicit assumption "there's always at least one duration" was never challenged during development; the bug surfaced only once real production traffic included a genuinely empty window, an edge case that's easy to overlook precisely because it's uncommon rather than because it's hard to reason about once you think to check for it.
How it was tested: added an explicit unit test calling the function with an empty list and asserting it returns None rather than raising, alongside the existing non-empty-input tests, so the specific edge case that caused the production issue is now permanently covered and can't silently regress.
What was learned to avoid similar bugs: for any function that aggregates over a collection, explicitly consider and test the empty-collection case as a matter of habit, not just when a bug report forces the question; more broadly, the pattern generalizes to any input assumption implicit in code (non-null, non-negative, within some expected range) that reads as "obviously always true" during development but isn't guaranteed by the function's actual contract, and is worth deliberately challenging each such assumption with a test rather than trusting that realistic-looking sample data will happen to cover it.
Trade-offs and pitfalls
The fix here (returning None for an empty input) is a specific design choice, not the only valid one; an alternative would be raising a more informative, explicit exception (ValueError("cannot average an empty list")) if silently returning None risks a caller mishandling it further downstream without noticing. The right choice depends on what callers actually need to do with a "no data" result, and is worth deciding deliberately rather than defaulting to whichever felt fastest to write.
Unlock Full Question Bank
Get access to all 25 Debugging and Systematic Troubleshooting interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.