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 build automated tests that catch when an upstream API or data source silently changes its response schema, before that bad data reaches production consumers?
Sample Answer
Direct answer
Add a schema-validation step at the ingestion boundary, using something like JSON Schema or Pydantic, that runs on every real response as part of the pipeline, not just at test time, so drift fails fast with a clear error instead of silently flowing downstream. Pair that with a scheduled contract test against the live upstream, not just mocked fixtures, so the team learns about a breaking change before a batch job does.
Structured elaboration
- Define the contract explicitly: required fields, types, and value constraints, not just "the response was 200 OK."
- Validate at the boundary: check every real response against the schema as it enters your system, and quarantine records that fail rather than letting a type mismatch propagate through several transformations before it's hard to trace back.
- Contract-test against the real upstream, scheduled independently of your own deploys: mocked fixtures will happily keep passing forever after the real API changes, because the mock never changes.
- Version and alert on schema changes, even for fields you don't currently use, since today's ignored field can become tomorrow's dependency.
Worked example
A pipeline ingests a partner's product feed where "price" was always a decimal like 19.99. The partner changes their API to a nested object with an amount in cents and a currency code. A schema check asserting price is a number fails immediately with "price: expected number, got object" and quarantines the batch, instead of the pipeline coercing the object to a missing value and silently loading zero-priced products.
Trade-offs and pitfalls
An overly strict schema that rejects any unknown extra field creates noisy failures every time upstream adds something unrelated; a good contract distinguishes fields you depend on from fields you ignore. Mock-only contract tests give false confidence because the mock and the real API can drift apart silently for months.
What the interviewer probes next
Whether the candidate distinguishes a test that checks your own parsing code from a test that checks the upstream contract itself.
You're designing automated test cases for a signup form that validates age, email format, and phone number. Walk me through how you decide which negative and boundary cases to include, and how you'd keep that test suite maintainable as the validation rules change over time.
Sample Answer
Direct answer
Use equivalence partitioning to group inputs into classes that should behave the same way, then pick one representative from each class plus the boundary between classes, instead of testing every possible value. Keep the suite maintainable by treating validation rules as data the test runner reads, not hardcoded assertions, so a rule change is a one-line diff.
Structured elaboration
- Equivalence classes: for age, a valid class (say 18-120), too-young, too-old, non-numeric, and empty. For email, valid format, missing @, missing domain, empty string, extremely long string. For phone, valid length/format, wrong country code, letters mixed in, empty.
- Boundary values: test the edges of each class (17 vs 18, 120 vs 121) because off-by-one errors cluster there.
- Control combinatorial growth: don't cross every field's negative cases with every other field's; test one field's negative cases while the rest stay valid, and reach for pairwise combinations only if fields genuinely interact.
- Maintainability: express the rules as a data-driven table (field, rule, valid/invalid examples) that the test suite reads from, or at least keep the table next to the validator in source control, so a rule change updates one place.
Worked example
For an age field valid from 18 to 120: test 17 (just below), 18 (valid boundary), 120 (valid boundary), 121 (just above), 0, a negative number, "abc", and an empty string. That is 8 cases covering all 5 equivalence classes (valid, too-young, too-old, non-numeric, empty) and both boundaries, not an exhaustive sweep of every possible age.
Trade-offs and pitfalls
Testing every combination of three fields' negative cases creates a combinatorially large, slow, brittle suite; testing only the happy path plus one negative case misses real bugs. A common pitfall is hardcoding the limits (18, 120) directly into test assertions, so the suite keeps "passing" even after the business rule changes to 21 and quietly stops testing anything meaningful.
What the interviewer probes next
Whether the candidate treats test data as a maintenance liability and not just a way to hit line coverage, and whether they would catch a rule change that makes existing tests pass for the wrong reason.
You're testing a checkout flow, and the payment provider's sandbox is configured to time out on demand. As a tester with no access to the source code, how would you verify the application handles that failure gracefully instead of hanging or crashing?
Sample Answer
Direct answer
Treat it as black-box behavioral testing: trigger the timeout deliberately using the sandbox's controls, then observe everything the user and the system state can tell you. Does the UI show a clear message within a bounded time rather than spinning forever, does retrying create a duplicate order, and does the order end up in a consistent state afterward.
Structured elaboration
- Define "graceful" concretely before testing it: a bounded wait time before feedback instead of hanging indefinitely, no browser tab crash or unhandled white-screen error, a clear and actionable message rather than a generic error, no duplicate charge on retry, and a final system state that's consistent even if degraded.
- Trigger the failure repeatably using the sandbox's timeout simulation rather than a real network failure, so the test is deterministic and repeatable, not a flaky race against real infrastructure.
- Check both sides of the transaction: the user-facing behavior and the backend state, does the order end up pending, failed, or duplicated, checked via an admin view or API even without source access.
- Test the retry path specifically: after the timeout, retry the checkout and verify it doesn't create a second charge or order, the black-box angle on idempotency.
- Time-box the wait: assert the user sees feedback within a defined limit, not "eventually."
Worked example
Using the payment sandbox's forced-timeout flag, submit a checkout for a fixed amount. Assert the UI shows a "payment couldn't be confirmed, please try again" message within roughly the payment timeout window rather than spinning indefinitely, then retry the same checkout and confirm via the order history that exactly one order for that amount exists, not two, and that no order shows a paid status without a corresponding successful charge.
Trade-offs and pitfalls
Testing only the failure trigger and not the retry leaves the most common real-world scenario, a user retrying after a timeout, unverified. Without source access, some invisible failure modes, like a duplicate charge recorded only in the payment provider's own dashboard, require coordinating with a developer or checking that provider's test dashboard, so pure black-box testing has a real limit here.
What the interviewer probes next
Whether the candidate thinks past "does an error message show up" to the retry and consistency checks that catch the expensive bugs, duplicate charges and inconsistent state.
You find two bugs while testing: one causes an unhandled exception and crash on a rare input, and one shows a confusing but harmless error message on a common input. How do you decide which to prioritize, and what would change your answer?
Sample Answer
Direct answer
Severity and priority are two different axes: severity is about impact, a crash is more severe than a confusing message, while priority also factors in frequency and user-facing cost, so I weigh how bad it is when it happens against how often it happens and who it affects. A rare crash can still outrank a common cosmetic issue if it causes data loss, but a common confusing message generating a high volume of support tickets might be the more urgent fix in practice.
Structured elaboration
- Severity axis: does it crash the app, corrupt data, or expose sensitive information (high), versus a confusing but recoverable message (low to medium).
- Frequency axis: how many users actually hit this input in practice; a rare edge case matters less in absolute impact even if individually severe.
- Business context that changes the answer: is the crash on a path used by a tiny fraction of users doing something unusual, or is it on the checkout flow; is the confusing message generating support tickets or silently confusing users who give up.
- Write it up so the decision is legible: file both with clear severity and frequency reasoning, how you triggered it, how common that input is in production if you can estimate it, so the team makes the priority call with real information, not just your gut.
Worked example
The crash happens when a user pastes an emoji into a field that isn't emoji-aware, likely hit by a tiny fraction of users. The confusing error message appears whenever a session expires mid-form, which happens to a meaningful share of users daily. Even though the crash is more severe in isolation, I'd flag the confusing message as higher priority to fix first because of how often it's hit, while still filing the crash as high severity so it doesn't get lost.
Trade-offs and pitfalls
Always fixing by severity alone ignores real-world impact and can spend a release cycle on a bug almost no one will see; always fixing by frequency alone risks leaving a rare but data-corrupting bug in production. The pitfall in bug reports is conflating severity and priority into one field, hiding the reasoning from whoever makes the final call.
What the interviewer probes next
Whether the candidate treats this as a real trade-off requiring context, rather than reciting "always fix crashes first," since a good QA engineer pushes back on oversimplified rules with actual reasoning.
You're handed a new feature to test with no written spec for what should happen on invalid input. How do you approach finding the input-validation gaps a developer might have missed?
Sample Answer
Direct answer
I'd start from the feature's purpose and reasonable user intent to infer what "invalid" should mean, then combine the standard technique, equivalence classes and boundaries on every field, with exploratory testing that deliberately tries inputs a real user or an attacker might realistically produce. I'd document each finding as a question for the developer or product owner rather than assuming my own guess is the spec.
Structured elaboration
- Infer intent from context: what is this field for, and what would a reasonable value look like; that gives you the "valid" class even without a written spec.
- Apply the standard techniques anyway: equivalence partitioning and boundary values still apply, you're just deriving the classes from context and common sense instead of a requirements document.
- Explore adjacent surfaces: unicode, extremely long strings, leading or trailing whitespace, copy-pasted content with hidden characters, concurrent submissions, browser back-button resubmission, the gaps a developer commonly misses while focused on the happy path.
- Log every ambiguous case as a question, not a silent judgment call: what should happen with a very long input in this field is a legitimate open question to raise with the team, not something to unilaterally decide.
Worked example
Testing a new "apply coupon code" field with no written spec, I'd try an empty string, a valid-looking but nonexistent code, a code with trailing whitespace, the same code submitted twice rapidly, a code belonging to an expired promotion, and an extremely long string. If a very long input crashes the page instead of showing "invalid code," that's a clear defect regardless of the missing spec; if the whitespace-trimming behavior is unclear, I'd raise it as an open question rather than guess.
Trade-offs and pitfalls
Spending unlimited time exploring every conceivable input isn't practical under a deadline; risk-based prioritization, what's most likely to be hit by real users, what's most costly if wrong, has to bound the exploration. The pitfall of exploratory testing without a spec is treating your own assumption about correct behavior as ground truth and filing "bugs" that are actually undocumented, intentional behavior.
What the interviewer probes next
Whether the candidate distinguishes "this crashes, definitely a bug" from "this behavior seems off, but I should confirm intent," since conflating the two either creates noise or lets real bugs slide as maybe-intentional.
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.