Code Quality, Error Handling, and Defensive Programming Questions
Writing robust, high-quality code that fails safely. Covers defensive programming, input validation, error handling and fault tolerance, logging for diagnosability, and general engineering-quality standards. Includes anticipating failure modes and making code resilient to bad inputs and unexpected states.
How would you verify, before an incident happens, that your circuit breakers and timeouts actually work as intended, rather than trusting they do because the code was reviewed?
Sample Answer
Direct answer
Deliberately inject the failure the mechanism is meant to handle, kill a dependency, add latency, drop connections, in a controlled environment and observe whether the circuit breaker actually trips, the timeout actually fires, and the system actually recovers when the dependency comes back. Code review confirms the logic looks right; only exercising the real failure path confirms it behaves right under conditions like connection-pool exhaustion that are hard to reason about statically.
Structured elaboration
- Fault injection tools: introduce latency, errors, or drops at the network layer, using a service mesh's fault injection or a tool like Toxiproxy, between the service and its dependency, rather than mocking the dependency, so the test exercises the real timeout and retry code paths.
- What to assert: the breaker opens within the expected failure count or window, requests fail fast (not hang) once it's open, and it attempts to close again (half-open) once the dependency recovers, without immediately re-overwhelming a barely-recovered dependency.
- Start in staging, graduate to production: run the same fault injection in staging first, then a controlled game-day exercise in production during low-traffic hours with the team watching dashboards, before trusting it to hold up during a real incident.
- Regression protection: once verified, add it to a scheduled chaos test so a future refactor that accidentally breaks the timeout configuration gets caught automatically instead of at the next real incident.
Worked example
A service has a 2-second timeout and a breaker configured to open after 5 consecutive failures. Injecting 10 seconds of latency on the downstream dependency, the test asserts requests time out around 2 seconds rather than hanging, the breaker opens after the 5th failure so the 6th request fails immediately, and after removing the injected latency and waiting the reset window, the next request succeeds and the breaker closes.
Trade-offs and pitfalls
Running fault injection in production carries real risk, so it needs a blast-radius limit, a single instance or a low-traffic canary window, and a fast abort mechanism. A common pitfall is testing that the breaker opens but never testing the half-open recovery behavior, which is where thundering-herd bugs, every waiting client retrying the instant the breaker closes, tend to hide.
What the interviewer probes next
Whether the candidate would actually test the recovery path, not just the failure-triggering path, since that's the part teams usually skip.
Describe the role of assertions and invariants in maintaining code correctness. When should assertions be used versus throwing exceptions? Provide an example where an assertion detects a developer error early and avoids a costly runtime check in production, and describe how this maps to design-by-contract thinking (preconditions, postconditions, invariants).
Sample Answer
Direct answer
An assertion checks a condition that your own code's logic guarantees should always be true if nothing upstream has a bug; an exception handles a condition that the outside world (a user, a file system, a network) can legitimately produce regardless of whether your code is correct. Assertions catch developer errors early and cheaply; exceptions handle the world being unpredictable.
Structured elaboration
What an assertion is for. An assertion encodes an invariant: a statement that, given correct code, must hold at this point in the program no matter what valid input arrives. If it fails, the bug is in the code that led to this point, not in the input or the environment. Because of this, assertions are cheap to reason about (you never need a recovery path for them, the correct response to a failed assertion is to fix the bug) and, in several languages, can be compiled out entirely in optimized production builds, which is precisely why they must never be relied on for something the outside world can trigger.
What an exception is for. An exception handles a condition that is a normal, expected possibility given a correct program: a file that does not exist, a network call that times out, a user who submits invalid input. These require an actual recovery path (retry, a default, informing the user) because they will happen in production no matter how correct the code is.
Preconditions, postconditions, and invariants (design by contract). A precondition is what a function requires to be true of its inputs to behave correctly; a postcondition is what it guarantees to be true of its output if the precondition held; an invariant is a condition that must hold at every observable point in an object's lifetime. Assertions are the natural implementation mechanism for all three inside a single codebase's own internal logic: asserting a precondition at function entry catches a caller who violated the contract due to a bug in their own code, which is different from validating an input that arrived from outside the trust boundary and might be malformed for entirely legitimate reasons.
The line between the two. The test is not "is this input bad", it's "could this input be bad even if every line of my own code is correct". A negative array length passed internally between two functions you wrote and control, where nothing external can produce that value if your code is right, is an assertion case. A negative quantity field parsed from a JSON request body is an exception (or validation-error) case, because a malicious or buggy client can produce it no matter how correct your server code is.
Worked example
A function withdraw(account, amount) internal to a ledger system might assert assert account.balance >= 0, "invariant violated: account balance went negative" right after debiting, because if the debit logic is correct, the balance should never go negative; if this assertion fires, there's a bug in the debit logic itself, and the fix is to find that bug, not to add a check that reacts gracefully to a negative balance in production. Contrast this with the same function's very first line, which must instead RAISE an exception (not assert) if the caller passes a negative amount: a negative withdrawal amount is exactly the kind of thing an upstream caller (a request handler parsing user input) can produce, whether or not the ledger's own internal logic has any bugs at all, so it needs a real, always-active check, and the right response is to reject the request, not to crash a debug build's assertion and silently no-op in production.
Trade-offs and pitfalls
The most costly mistake is using an assertion to validate something a real caller can trigger: because assertions can be disabled in optimized builds in several languages (C's NDEBUG, Python's -O flag), a check that only exists as an assertion can silently vanish in production, meaning the exact case it was meant to guard against reaches production code entirely unchecked. The opposite mistake, wrapping every internal invariant in a full exception with a try/catch elsewhere in the codebase, adds real performance and readability cost for something that, if your own code is correct, should genuinely never happen and does not need a recovery path at all, only a way to fail loudly and immediately during development and testing.
What is fuzz testing, and how would you use it to find defensive-programming gaps, like unhandled exceptions or crashes, in a service that parses untrusted file uploads?
Sample Answer
Direct answer
Fuzz testing feeds a program large volumes of automatically generated, malformed, or mutated input to find inputs that cause crashes, hangs, or exceptions the developer didn't anticipate. For a file-upload parser, point the fuzzer at the parsing function directly, not through the full HTTP stack, and let it mutate a corpus of valid sample files over time.
Structured elaboration
- Two flavors: mutation-based fuzzing takes valid seed files and randomly flips bits or truncates chunks; generation-based fuzzing builds inputs from a grammar or format spec. For file formats, mutation-based (starting from real valid files) usually finds bugs faster.
- Coverage-guided fuzzing tracks which code paths each input exercises and prioritizes inputs that reach new paths, exploring the parser's edge cases far more efficiently than pure random input.
- What it catches that manual tests miss: buffer over-reads, integer overflows in a length field, infinite loops on a malformed length header, uncaught exceptions on a deeply nested or recursive structure.
- Triage: a crashing input needs to be minimized to the smallest reproducer and traced back to a specific missing bounds check or unhandled exception type.
Worked example
Fuzzing an image-upload parser by mutating a valid PNG's header bytes surfaces an input where the declared image width is a huge number that overflows a buffer-size calculation, causing the parser to attempt a far larger allocation than the file itself and crash, a defensive gap a handful of manually written test files would likely never stumble onto.
Trade-offs and pitfalls
Fuzzing is good at finding "this input crashes the program" but bad at finding "this input parses to the wrong business meaning," so it complements rather than replaces functional tests. Running a fuzzer for a few minutes in CI without a seeded corpus often finds nothing and gives false confidence; effective fuzzing usually needs longer continuous runs and a maintained seed corpus.
What the interviewer probes next
Whether the candidate knows fuzzing is a targeted technique for parsers and input-handling code, not a general replacement for the rest of the test pyramid.
Implement bool add_will_overflow(int32_t a, int32_t b) in C++ that returns true if a + b would overflow a 32-bit signed integer. Do not use a 64-bit type. Include unit tests for edge cases such as INT_MAX + 0, INT_MAX + 1, and negative overflows, and explain your approach.
Sample Answer
Direct answer
Detecting whether a + b would overflow a 32-bit signed integer, without widening to 64 bits, means reasoning about the operation BEFORE it happens using only the bounds of int32_t itself: check whether b is positive and a is already close enough to the maximum that adding b would exceed it, and symmetrically for a negative b against the minimum.
Structured elaboration
Why you can't just compute a + b and check the result. Computing the sum first and then checking whether it looks wrong is undefined behavior for signed integer overflow in C++, meaning the compiler is permitted to assume overflow never happens and can optimize the check away entirely, silently producing incorrect results specifically in the case you were trying to detect. The check has to be done using only values that are guaranteed to be representable, before the actual addition occurs.
The two symmetric cases. If b is positive, overflow happens when a is already greater than INT32_MAX - b (equivalently, adding b would push past the maximum); this comparison, a > INT32_MAX - b, is always computable without overflow since INT32_MAX - b cannot itself overflow when b is positive. If b is negative, overflow (underflow past the minimum) happens when a is less than INT32_MIN - b; note INT32_MIN - b is safe to compute here specifically because b is negative, making this subtraction move away from, not toward, the boundary.
The zero and boundary cases. b == 0 never overflows regardless of a, and the two comparisons above naturally handle this correctly without a special case, since a > INT32_MAX - 0 is simply a > INT32_MAX, which is never true for a valid int32_t value of a.
Worked example
bool add_will_overflow(int32_t a, int32_t b) {
if (b > 0 && a > std::numeric_limits<int32_t>::max() - b) return true;
if (b < 0 && a < std::numeric_limits<int32_t>::min() - b) return true;
return false;
}
Executed and verified (g++, -Wall -Wextra): add_will_overflow(INT32_MAX, 0) is false (no overflow); add_will_overflow(INT32_MAX, 1) is true (the classic overflow case); add_will_overflow(INT32_MAX - 1, 1) is false (exactly at the boundary, still valid); add_will_overflow(INT32_MIN, -1) is true (the symmetric underflow case); add_will_overflow(INT32_MIN, 0) is false; add_will_overflow(INT32_MIN + 1, -1) is false (exactly at the boundary on the negative side); ordinary values like add_will_overflow(100, 200) and add_will_overflow(-100, -200) are both false; and add_will_overflow(INT32_MAX/2 + 1, INT32_MAX/2 + 1) is true, confirming the check also catches an overflow that occurs from two moderately-large positive values rather than only from a value already at the exact boundary.
Trade-offs and pitfalls
The single most common mistake is writing the intuitive-looking but broken version, int32_t sum = a + b; if (sum < a) return true; (checking whether the result "wrapped around" to something smaller than one of the inputs): this relies on signed overflow actually wrapping, which is undefined behavior in C++ and not guaranteed to behave that way at all, especially under compiler optimizations that are explicitly permitted to assume signed overflow never occurs and can eliminate the check entirely. A second, more subtle mistake is getting the comparison direction backwards for the negative-b case (checking a < INT32_MIN + b instead of a < INT32_MIN - b), which happens to work correctly by luck for some inputs and silently fails for others; testing both boundary directions explicitly, as in the worked example, is what catches this class of subtle sign error.
Design a small set of custom static-analysis checks to detect common defensive-programming anti-patterns that make debugging harder, for example swallowing exceptions, broad try/catch blocks, and empty catch blocks. For each check, explain how you would implement it and give a sample warning message.
Sample Answer
Direct answer
A custom static-analysis check for defensive-programming anti-patterns works by pattern-matching the SHAPE of a code construct in an abstract syntax tree, not by textual pattern matching, and each check should target one specific, well-defined anti-pattern (an empty catch block, a catch that only logs and swallows, a catch of the broadest possible exception type) with a clear, actionable warning message rather than trying to build one general-purpose "bad error handling" detector.
Structured elaboration
Empty catch blocks. The simplest and highest-confidence check: find any catch/except block whose body is empty, or contains only a comment, or contains only a pass/no-op statement. This is almost never intentional and almost always indicates an error that was silently discarded during debugging and never properly handled afterward. Implementation: walk the AST for exception-handler nodes and check whether the handler's body block contains zero meaningful statements.
Broad exception catching. Flag a catch clause that catches the broadest possible exception type (except Exception, catch (Exception e), a bare except: with no type at all) UNLESS it is the outermost handler in an entry point specifically responsible for preventing a whole process crash (a top-level request handler, a main loop), which is a legitimate and common exception to the rule; implementation-wise, this means the check needs a configurable allowlist of file paths or function names where broad catching is intentional, rather than flagging every instance uniformly.
Catch-log-swallow. A subtler variant of the empty-catch problem: a catch block that logs the exception (so it isn't literally silent) but then continues execution as if nothing happened, without either re-raising, returning an error indicator to the caller, or taking an explicit recovery action. This requires slightly more sophisticated analysis than the empty-catch case: the check looks for a catch block whose only statements are a logging call, with no re-raise, no return of an error value, and no explicit recovery logic.
Sample warning messages. For an empty catch block: "Empty catch block silently discards <ExceptionType>. Either handle it explicitly or, if this really is intentional, add a comment explaining why swallowing this specific exception is safe here." For overly-broad catching: "Catching the base Exception type here will also catch bugs unrelated to what this block is meant to handle (a NullPointerException from an unrelated coding mistake, for instance). Catch the specific exception type(s) you actually expect from this call."
Integrating into developer workflow without excessive noise. Run as a fast, incremental check on changed files in CI (not a full-codebase scan on every commit, which would be slow and would re-flag pre-existing violations that aren't part of this change), and provide a narrow, explicit suppression mechanism (an inline comment like # noqa: empty-except with a required justification) for the genuine, rare cases where the pattern is intentional, rather than either blocking every legitimate exception or having no escape hatch at all.
Worked example
A simplified AST-based check for empty catch blocks, sketched in Python-like pseudocode operating over a parsed syntax tree:
def check_empty_except(tree):
violations = []
for node in tree.walk():
if node.type == "except_handler":
body_statements = [s for s in node.body if s.type not in ("comment", "pass")]
if len(body_statements) == 0:
violations.append({
"line": node.line,
"message": f"Empty except block for {node.exception_type or 'bare except'}; "
f"handle it explicitly or document why swallowing it is safe."
})
return violations
Applied to try: risky_call()\nexcept Exception:\n pass, this flags line 2 with a message naming the caught type and asking for either real handling or an explicit, documented justification, rather than a generic "bad code" warning that gives the developer no specific next step.
Trade-offs and pitfalls
A check that flags EVERY broad exception catch with no allowlist mechanism will immediately generate noisy, unwanted warnings on legitimate top-level handlers (a web framework's outermost request handler, which SHOULD catch broadly to prevent one request's unexpected error from crashing the whole server process), and a team that gets flooded with warnings on code that's actually correct will quickly learn to ignore the tool entirely, defeating its purpose; a configurable allowlist for known-legitimate broad-catch locations is what keeps the signal-to-noise ratio high enough that the warnings are still trusted. The most common implementation mistake is building this as a simple TEXT-based regex search over source files rather than an AST-based check: a regex looking for except Exception will miss a semantically-identical broad catch written slightly differently (a caught type stored in a variable, or a multi-line catch clause), and will also produce false positives on the string appearing inside a comment or a string literal that has nothing to do with actual exception-handling code.
Unlock Full Question Bank
Get access to all 10 Code Quality, Error Handling, and Defensive Programming interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.