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.
A backend service builds a SQL query by concatenating a user-supplied search string directly into the query text. What's wrong with this from a defensive-programming standpoint, and what would you check for in code review to catch this class of bug at scale?
Sample Answer
Direct answer
Concatenating untrusted input directly into a SQL string lets an attacker change the query's structure, not just its data, which is SQL injection and can expose or modify anything the database connection can reach. The fix is parameterized queries, where input is always bound as data and can never be interpreted as SQL syntax; at scale, catching this means a static-analysis rule that flags any string concatenation feeding a query-execution call, not relying on a human to spot every instance.
Structured elaboration
- Why it's dangerous: if the search string is a SQL fragment like a statement terminator followed by a destructive command, a concatenated query executes it as SQL, because the database can't distinguish "data the user typed" from "query the developer wrote" once they're merged into one string.
- The fix: parameterized queries or an ORM's query builder, where the driver sends the query text and parameters separately, so the database always treats parameters as literal values.
- Scaling the catch beyond code review: a static-analysis rule, via a linter or a tool like Semgrep, that flags any database call built from string concatenation or interpolation, run in CI so it blocks the pull request automatically.
- Defense in depth: least-privilege database credentials, so the app's database user doesn't have permission to drop tables at all, shrinking the blast radius if a check is ever bypassed.
Worked example
Vulnerable: building the query text as SELECT * FROM users WHERE name = ' plus the raw user input plus '. Safe: SELECT * FROM users WHERE name = ? with the input passed as a bound parameter. With the vulnerable version, an input like x' OR '1'='1 turns the WHERE clause into always-true, returning every row instead of matching one name.
Trade-offs and pitfalls
Parameterization doesn't cover every injection surface; dynamically chosen table or column names can't be parameterized the same way and still need explicit allow-list validation, not string interpolation. A common pitfall is fixing the obvious query but missing a second one built the same way in a reporting script or admin tool that doesn't go through the same review path.
What the interviewer probes next
Whether the candidate reaches for "parameterize it" immediately, then goes further into how they'd find every other instance of the pattern across a large codebase, not just this one.
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.
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.
Compare and contrast graceful degradation and fail-fast design approaches for production systems. For each approach, explain a typical use case (for example a customer-facing API versus an internal pipeline), the operational trade-offs, how you would instrument each approach with metrics, logs, and traces, and how you would communicate degraded functionality to clients or downstream systems.
Sample Answer
Direct answer
Fail-fast stops the operation immediately and surfaces the error the moment something is wrong, trading availability for correctness and a clear signal; graceful degradation keeps the system partially functional by falling back to reduced capability, trading some correctness or completeness for continued availability. The right choice depends on whether a wrong or incomplete answer is worse than no answer at all for this specific system.
Structured elaboration
Fail-fast: when correctness matters more than availability. An internal data pipeline computing financial reconciliation numbers should fail fast and loudly the moment its inputs look wrong, because a wrong number that looks plausible and gets used in a report is a much worse outcome than the pipeline simply not running today. Fail-fast systems are also easier to operate: a hard failure with a clear error is diagnosable immediately, whereas a system that silently degrades can mask a real problem for a long time before anyone notices the quality of its output has quietly dropped.
Graceful degradation: when partial availability beats a hard stop. A customer-facing product page that depends on a recommendation service should degrade to a generic, non-personalized set of recommendations if that service is slow or down, rather than showing the customer an error page, because a slightly-worse-but-functional page is a much better outcome for both the customer and the business than a hard failure on a page that otherwise works fine.
Instrumentation differs by approach. A fail-fast system needs strong alerting on the failure itself, since the failure IS the signal: an error rate spike, a specific exception type, or a circuit breaker opening. A gracefully-degrading system needs the opposite kind of visibility: a metric or log line specifically for "we are currently in degraded mode", because the degraded path, by design, does not look like a failure to a simple error-rate dashboard, and a team that isn't specifically tracking degraded-mode usage can be running in a permanently degraded state for months without noticing.
Communicating degraded functionality. For a user-facing system, this usually means a visible but non-alarming UI signal ("Showing popular items while personalized recommendations are unavailable") rather than silence, since silent degradation erodes trust once a user notices the quality difference without being told why. For a downstream service-to-service dependency, this means an explicit field or header in the response indicating degraded mode, so the calling service can make its own informed choice about whether to also degrade or to fail.
Worked example
An internal pipeline vs. a downstream API dependency, side by side: the internal pipeline computing quarterly revenue numbers for a financial report should fail fast and halt if a required upstream table is empty or a row count sanity check fails, alerting the on-call data engineer immediately, because publishing a subtly wrong number in a financial report is far worse than the report being late. The customer-facing recommendation widget on the same company's storefront, dependent on a separate ML service, should instead catch a timeout from that service and immediately serve a cached "most popular this week" list, log a degraded_mode=true metric tagged with the reason, and continue serving the page, because an incomplete page for one widget is a minor UX cost, not a correctness failure the business needs to halt over.
Trade-offs and pitfalls
The most common mistake is applying the wrong default to a whole system uniformly: treating every dependency as fail-fast produces a fragile product where one non-critical service outage takes down an entire page, while treating every dependency as gracefully-degradable risks quietly serving wrong financial or safety-relevant data with no alert ever firing. The decision should be made dependency by dependency, based on whether being wrong is worse than being unavailable for that specific piece of functionality, not applied as a single system-wide policy.
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.