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.
Your daily ETL pipeline processes millions of records where roughly 0.1% are malformed in unanticipated ways, and the job occasionally crashes mid-batch when the orchestrator restarts it. Describe your defensive design for both problems: how bad records get isolated instead of corrupting downstream tables, and how a restart avoids double-processing or losing records.
Sample Answer
Direct answer
For malformed records, validate each record against a schema at ingestion and route failures to a dead-letter store instead of letting them stop the batch or corrupt downstream tables, turning 0.1% bad data into a bounded, monitored side channel. For crash-safe restarts, make the job idempotent by processing in checkpointed, replayable units, so a restart resumes from the last committed checkpoint instead of reprocessing everything or skipping records.
Structured elaboration
- Malformed-record isolation: validate required fields and types before transformation; a record that fails validation is written to a dead-letter table with the original payload and the specific error, and the job continues with the remaining records rather than failing the whole batch on one bad row.
- Alert on the dead-letter rate, not just its presence: 0.1% might be expected background noise or a new bug; alert when the rate deviates from baseline, since a hardcoded "zero bad records" threshold guarantees constant false alarms.
- Crash-safe restart via checkpointing: process in small, idempotent units, for example per-partition or per-batch, and commit a checkpoint only after that unit's output is durably written; on restart, resume from the last committed checkpoint.
- Idempotent writes: make the downstream write itself safe to repeat, using an upsert keyed by a natural id rather than an append, so if a batch is reprocessed after a crash, the result matches processing it once, not duplicated rows.
Worked example
A pipeline processes daily transaction files in batches of 10,000 records, checkpointing after each batch's output is durably upserted to the warehouse. If the job crashes after committing batch 47 but before finishing batch 48, the restart resumes at batch 48, not batch 1. Within batch 48, a record with an invalid date fails schema validation, is written to a dead-letter table with the error "invalid date format," and processing continues with the remaining 9,999 records.
Trade-offs and pitfalls
Checkpointing after every single record adds overhead for little benefit; checkpointing once per whole file means a crash near the end reprocesses almost everything, which is only safe if the writes are truly idempotent. A common pitfall is building dead-letter isolation for malformed records but forgetting to make the checkpoint-and-restart logic idempotent, so bad-data handling looks solved while a crash still silently duplicates good records.
What the interviewer probes next
Whether the candidate connects the two problems, idempotent writes are what make checkpointed restarts actually safe, rather than treating them as two unrelated asks.
A debug log statement accidentally includes a raw API key or password in plaintext, and it ships to production before anyone catches it in review. How do you prevent this class of bug systematically, not just rely on catching it in the next code review?
Sample Answer
Direct answer
Treat it as a tooling and process gap, not a reviewer-attention gap: add automated secret scanning at commit time and in CI so a credential-shaped string never merges, and add a logging-layer guard that redacts known secret field names before anything is written out, regardless of what a developer typed. Rotate the exposed credential immediately once found, because a log line, once written, has to be treated as compromised even after the code is fixed.
Structured elaboration
- Prevention layer 1, secret scanning: tools like gitleaks or truffleHog run in pre-commit and CI, catching a credential-shaped string before it's even committed, earlier than code review would.
- Prevention layer 2, structured logging with a deny-list: a logging wrapper that inspects known sensitive field names (password, apiKey, token, authorization) and masks them, so even a developer who forgets is still protected by the framework.
- Prevention layer 3, log-destination scanning: tools on the aggregated log store itself that alert if a secret-shaped pattern appears in ingested logs, catching what slipped past the first two layers.
- Response when it happens anyway: rotate the credential immediately; a log line lives in multiple systems (aggregator, backups, a SIEM), so deleting the log line doesn't undo the exposure.
Worked example
A developer adds a debug line that logs "calling payment API with key: " plus the raw key. A pre-commit gitleaks hook configured with a pattern for the vendor's key format flags the commit before it's ever pushed, well before it would reach a human reviewer or production.
Trade-offs and pitfalls
Deny-list redaction by field name misses a secret logged under an unexpected key or embedded in free text, so it's a safety net, not a guarantee, which is why scanning before merge matters more than redaction at log time. Overly aggressive secret-pattern matching produces false positives that train developers to bypass the scanner, so the patterns need tuning to real credential formats.
What the interviewer probes next
Whether the candidate's answer is "we'd catch it in review," which is exactly what already failed here, versus a systemic, automated, multi-layer answer.
Why do liveness and readiness checks need to be defensive about what they actually verify, and what's an example of a health check that lies about system health?
Sample Answer
Direct answer
A health check that returns success just because the process is running, without checking anything it actually depends on, lies by reporting healthy while the service can't do its job, for example because its database connection pool is exhausted. Liveness checks should answer "should this process be restarted," and readiness checks should answer "can this instance serve traffic right now," and conflating the two causes an orchestrator to make the wrong decision.
Structured elaboration
- Liveness check: answers "should this be restarted?" It should catch deadlocks or unrecoverable internal state, but should not check external dependencies, because a database outage isn't fixed by restarting the app, and a liveness check that fails on a database outage causes the orchestrator to restart every instance simultaneously, making the outage worse.
- Readiness check: answers "should traffic go to this instance right now?" This one should check dependencies, database reachable, cache reachable, disk space available, because an instance that can't reach its database shouldn't receive traffic even though it doesn't need to be restarted.
- The lying health check anti-pattern: an endpoint that just returns success with no logic reports healthy even when every downstream dependency is down, the single most common health-check bug in production systems.
- Defensive design: a readiness check needs its own short timeout and shouldn't itself become a source of load, since orchestrators poll it frequently.
Worked example
A service's health endpoint returns success as long as the HTTP server thread is alive, with no check of its database connection. The database goes down; the app is still "alive" and keeps receiving traffic, returning errors to every real request, while the health check keeps reporting green the entire time, hiding the outage from the orchestrator.
Trade-offs and pitfalls
Making a liveness check too strict, checking dependencies, causes cascading restarts during a dependency outage, turning a partial outage into a full one. Making a readiness check too shallow, like the example above, means the orchestrator keeps sending traffic to instances that can't serve it, arguably worse than doing nothing.
What the interviewer probes next
Whether the candidate distinguishes liveness from readiness at all, since conflating them is extremely common and causes real incidents.
When an authorization check throws an unexpected error, should the system fail open and allow the action, or fail closed and deny it? Walk me through how you'd decide, using a concrete example.
Sample Answer
Direct answer
For security-sensitive decisions like authorization, default to fail closed, deny the action when the check can't complete, because the cost of a false negative (briefly blocking a legitimate user) is almost always smaller than the cost of a false positive (an unauthorized action succeeding). The exception is when failing closed itself creates a worse safety or availability problem, which is a deliberate, domain-specific judgment call, not a blanket rule.
Structured elaboration
- Default posture: authorization and permission checks fail closed. If the permission service times out, deny the request with a clear error; don't silently treat "unknown" as "allowed."
- Where fail-open is sometimes deliberate: low-stakes, availability-critical paths where an outage of the check itself would be worse than the risk it guards against, and the exposure window is bounded and monitored.
- Make the decision explicit and documented per check, not an accident of how the code happens to be written; a try/catch that swallows the exception and falls through is an accidental fail-open.
- Instrument it: a fail-closed denial caused by an internal error should be logged and alerted distinctly from a legitimate permission denial, so an outage in the auth path shows up as an incident, not just a spike in 403s.
Worked example
An admin panel calls a permissions service to check if a user can delete a record. The service times out. If the code reads let allowed = true; try { allowed = permissionCheck(); } catch (e) { logger.warn(e); } followed by if (allowed) { allowDelete(); }, the caught exception is logged but never rethrown, so allowed is left at its optimistic default of true and the timeout results in the delete going through anyway, an accidental fail-open hiding inside code that looks defensive because it has a try/catch. The fix flips the default and the exception path: let allowed = false; try { allowed = permissionCheck(); } catch (e) { return deny(); }, so any failure to get a definitive answer denies the action.
Trade-offs and pitfalls
Fail-closed can turn a partial outage of a dependency into a full outage of everything gated behind it, so it needs to be paired with making that dependency itself highly available (caching the last known-good permission, short timeouts, circuit breakers) rather than accepting cascading denial as the cost of security. Fail-open, chosen without discussion, is the classic way an authorization bug ships silently, because everything still "works" in testing.
What the interviewer probes next
Whether the candidate can articulate the asymmetry between a false allow and a false deny, rather than reciting "fail closed is always right," and whether they'd catch an accidental fail-open hiding inside ordinary-looking exception handling.
Unlock Full Question Bank
Get access to all 12 Code Quality, Error Handling, and Defensive Programming interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.