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.
A boundary check validates that a value (an index, an offset, a size) falls within the range the code actually handles correctly, and it routinely catches real production bugs before they cause damage. Pick three DIFFERENT kinds of boundary bugs you've seen or can construct realistically, and for each: describe the bug it would cause if unchecked, the specific defensive check you'd add, and a unit test that would catch a regression if the check were later removed.
Sample Answer
Direct answer
A boundary check catches a specific class of bug (accessing an index, offset, or value outside the range the code actually handles correctly) at the moment it happens, instead of letting it silently produce wrong output or crash somewhere unrelated later; three concrete examples: array/list indexing, pagination offsets, and numeric limits.
Structured elaboration and worked examples
- Array indexing: the bug is an off-by-one or attacker-controlled index reading past the end of a buffer or list. The defensive check: validate
0 <= index < len(array)before accessing, raising a clearIndexError/custom exception instead of either crashing with a cryptic native error or, in an unsafe language, reading adjacent memory. A unit test:assert_raises(IndexError, get_item, [1,2,3], 5). - Pagination offsets: the bug is a negative or absurdly large
offset/limitfrom a client, which can either error confusingly deep in a SQL driver or, worse, silently return zero rows and look like 'no data' rather than 'bad request'. The defensive check: clamp or rejectoffset < 0and caplimitto a sane maximum (say 1000) before it reaches the query layer. A unit test:assert paginate(items, offset=-5, limit=10) raises ValueError. - Numeric limits: the bug is an integer overflow or an out-of-domain value (a negative quantity in an order, a percentage over 100) silently producing a nonsensical result instead of an error. The defensive check: validate the value's range explicitly before using it in a calculation. A unit test:
assert_raises(ValueError, apply_discount, price=100, percent=150).
Trade-offs and pitfalls
Each of these checks is cheap individually, but the value comes from applying them CONSISTENTLY at every place the boundary is actually crossed (every array access from external input, not just the ones you happen to remember); a single unguarded pagination endpoint added six months later by someone who didn't see this pattern reintroduces the exact bug class. Treat these as patterns to lint for or wrap in a shared utility function, not as one-off checks to remember individually.
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.
A developer adds a null-check to fix a crash bug. What regression risk does this kind of small defensive fix introduce, and how would you test to make sure the fix doesn't quietly swallow a case that should have been surfaced as an error?
Sample Answer
Direct answer
The risk is that a null-check written to stop a crash can silently convert "this should have been an error" into "this now does nothing," trading a loud, visible bug for a quiet, harder-to-detect one. Test by checking what the code now does with a null input beyond "it doesn't crash," specifically whether the surrounding logic still behaves correctly or now silently produces a wrong result.
Structured elaboration
- The over-defensive pattern to watch for: an
if (value != null)guard with no else branch fixes the crash but also means the null case now produces no error, no log, and no signal, the operation simply doesn't happen. - What to test beyond "no crash": trace what happens downstream when the null case occurs, does a required step get silently skipped, does the function return an empty or default result that looks like a legitimate answer, does anything log that this path was hit at all.
- Ask "should this actually be an error?": the right regression test isn't "assert no exception," it's "assert the correct behavior," which might mean the fix should raise a validation error or return an explicit failure instead of silently skipping.
- Add an explicit test for the previously-crashing input that asserts on the meaningful outcome, a specific error, a specific fallback value, a log entry, not just "the suite doesn't throw," so a future refactor can't silently make it wrong again while still passing.
Worked example
A function that calculates a discount crashed with a null-pointer exception when the coupon argument was null. The fix adds a null guard around applying the discount. The crash is gone, but now a null coupon, which happens when a coupon expires mid-checkout, silently applies zero discount with no error and no message, when the correct behavior should tell the user their coupon expired. A regression test that only asserts "calling with null doesn't throw" would pass on this broken fix; a better test asserts the user sees an expired-coupon message and the discount is explicitly zero for a stated reason, not just silently absent.
Trade-offs and pitfalls
Not every null-check is wrong, sometimes silently doing nothing genuinely is correct behavior for an optional field; the risk is specifically when null represents an error condition being quietly absorbed. The broader pitfall is a test suite that only measures "does it crash," which gives a false sense of safety, since a defensive fix that trades a crash for a silent wrong answer makes that metric look better while making the actual bug worse.
What the interviewer probes next
Whether the candidate would push back and ask "should this actually have been an error" rather than accepting "the crash is fixed" as sufficient.
You need to verify that a payment endpoint is safe to retry, meaning a client that times out and retries doesn't create a duplicate charge. How would you test this, and what would the endpoint need to implement to make it testable?
Sample Answer
Direct answer
The endpoint needs an idempotency key, a client-generated unique id sent with the request, so the server recognizes a retry as "the same request" and returns the original result instead of processing it twice. To test it, simulate the exact failure, the server processes the request but the response is lost, and the client retries with the same key, then assert only one charge exists.
Structured elaboration
- Implementation requirement: the server stores (idempotency key, request hash, result) and on a repeat key returns the stored result instead of re-executing side effects, typically with a TTL and a check that the retried body matches the original.
- Test 1, happy-path retry: send request A with key K, capture the result; resend identical request A with key K; assert the second call returns the same result and no second charge is created.
- Test 2, concurrent retry: fire two requests with the same key K at nearly the same time, simulating a client retrying before the first response returns; assert exactly one charge is created. This tests that the check is atomic, not "check then insert" with a race window.
- Test 3, key reuse with a different payload: send key K with amount 10, then key K with amount 20; assert the defined behavior (reject as a conflict) rather than silently using either amount.
- Test 4, TTL expiry: confirm the deliberately chosen, tested behavior when the same key is reused after the idempotency record has expired.
Worked example
A checkout service processes a 50-unit charge under key "order-4471-attempt-1". The client's connection drops after the charge succeeds but before the response arrives, so it retries with the same key. The test asserts the payment gateway shows exactly one 50-unit charge and the API returns the same transaction id both times, not a new one.
Trade-offs and pitfalls
The most common bug is a "check if key exists, then insert" pattern that isn't atomic; it passes sequential tests but fails under real concurrent retries, which is why Test 2 is the one that actually matters and the one teams skip. Storing idempotency records forever is a data-growth problem, but too short a TTL reopens the double-charge window during exactly the retry storms it exists to prevent.
What the interviewer probes next
Whether the candidate reaches for the concurrency test unprompted, since a sequential-only idempotency suite gives false confidence.
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.
Unlock Full Question Bank
Get access to all 11 Code Quality, Error Handling, and Defensive Programming interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.