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.
Write a small JavaScript function safeParseJSON(jsonString) that returns an object {success: boolean, value: any, error: string|null}. It must not throw, must handle invalid JSON gracefully, and must not allow prototype pollution (it must avoid assigning to proto or constructor). Also write one example unit test in Jest for an invalid input.
Sample Answer
Direct answer
A safe JSON parser must never throw on bad input, must report success or failure through its return value instead, and must specifically guard against prototype pollution, which is JSON.parse's own quiet trap: a payload like {"__proto__": {"polluted": true}} can corrupt the global Object prototype for the entire process if you are not careful about how you consume the parsed result.
Structured elaboration
Never throw. JSON.parse throws a SyntaxError on malformed input; a "safe" wrapper catches that and returns a structured result instead, so callers do not need a try/catch at every call site.
Prototype pollution. JSON.parse itself does not mutate Object.prototype (unless you additionally use a reviver or later merge the result unsafely into another object with a naive deep-merge). The real risk in a "safe parse" utility is what happens AFTER parsing: if calling code later does something like Object.assign(defaults, parsed) or a recursive merge without checking keys, an attacker-controlled __proto__ or constructor.prototype key can walk up to the shared prototype and add or override properties on every object in the process. The defensive fix at the parse boundary is twofold: use a JSON.parse reviver function that strips dangerous keys (__proto__, constructor, prototype) as soon as they are encountered, and additionally verify with Object.prototype.hasOwnProperty.call that the top-level parsed object does not carry one of those keys before returning it, since a reviver alone can miss certain nested shapes depending on how the object is later traversed.
Structured return, not an exception. Return {success, value, error} so a caller writes if (!result.success) { ...handle... } instead of a try/catch, which keeps the calling code linear and makes "parsing failed" an ordinary value instead of a special control-flow path.
Worked example
function safeParseJSON(jsonString) {
if (typeof jsonString !== 'string') {
return { success: false, value: null, error: 'input is not a string' };
}
let parsed;
try {
parsed = JSON.parse(jsonString, (key, value) => {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') return undefined;
return value;
});
} catch (err) {
return { success: false, value: null, error: err.message };
}
if (parsed && typeof parsed === 'object') {
if (Object.prototype.hasOwnProperty.call(parsed, '__proto__') ||
Object.prototype.hasOwnProperty.call(parsed, 'constructor')) {
return { success: false, value: null, error: 'disallowed key detected' };
}
}
return { success: true, value: parsed, error: null };
}
Executed (Node.js, verified): safeParseJSON('{"a": 1, "b": [1,2,3]}') returns {success: true, value: {a: 1, b: [1,2,3]}, error: null}. safeParseJSON('{not valid json') returns success: false with the underlying SyntaxError message, and does not throw. safeParseJSON(42) returns success: false without ever calling JSON.parse on a non-string. Critically, safeParseJSON('{"__proto__": {"polluted": true}}') was run and confirmed that ({}).polluted is undefined afterward, meaning the global Object prototype was NOT polluted, and the parsed value has no __proto__ own-key surviving in it; the same holds for a constructor.prototype variant of the attack.
One Jest unit test for the invalid-input case:
test('returns a failure result instead of throwing on malformed JSON', () => {
const result = safeParseJSON('{not valid json');
expect(result.success).toBe(false);
expect(result.value).toBeNull();
expect(typeof result.error).toBe('string');
});
Trade-offs and pitfalls
The reviver-based key strip handles the common shape of the attack, but a defense-in-depth mindset says: never rely on parse-time stripping alone if you also deep-merge untrusted objects elsewhere in the codebase, because a different merge utility downstream might not go through this parser at all. Prefer Object.create(null) or a Map for any object you build from untrusted keys if you can avoid prototype-based objects entirely. A common mistake is checking for __proto__ as an enumerable key with a plain for...in loop, which will not find it, since __proto__ set via the object literal syntax is an accessor, not an own enumerable property in the usual sense; using hasOwnProperty.call directly, as above, avoids that gap.
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.
While testing, you trigger a server error and see a full stack trace in the API response, including internal file paths. Is this worth filing as a bug, and how would you write it up so it gets fixed with the right urgency?
Sample Answer
Direct answer
Yes, this is worth filing, and it should be filed as a security-relevant information-disclosure issue, not just a cosmetic formatting bug, because a stack trace can reveal internal file paths, library versions, and code structure that helps an attacker map the system for further attacks. Write it up with the exact request that triggered it, the full response received, and an explicit classification so it routes to the right priority instead of being treated as "just an ugly error page."
Structured elaboration
- Why it matters beyond aesthetics: file paths reveal server structure, stack frames reveal library names and versions useful for looking up known vulnerabilities, and sometimes a trace includes a fragment of a query or config value.
- How to classify it: this is an information-disclosure finding; from a QA perspective the finding itself is what to report, whether or not a formal policy for safe error detail already exists.
- What to include in the report: the exact request that triggered the error, the full raw response body, whether it's reproducible, and an explicit note that this exposes internal implementation detail.
- Severity reasoning to include: is this reachable by an unauthenticated user (higher severity) or only after authentication and specific permissions (lower, but still real); does the exposed information look like it could feed a next-stage attack.
Worked example
Submitting a malformed date to a booking API returns a server error with a full stack trace showing an internal file path and the framework version. File it as "information disclosure: stack trace exposed on malformed date input," attach the exact request and response, note it's reachable without authentication, and flag it as security-relevant rather than routing it as a generic cosmetic error-message bug, since the fix and the urgency differ for each classification.
Trade-offs and pitfalls
Under-classifying this as a low-priority cosmetic bug is the main risk, since "the error message looks ugly" and "the error message leaks our internal file structure to anyone" get very different response times. Over-classifying every internal error message as critical without checking reachability can also cause alert fatigue that makes real findings get deprioritized.
What the interviewer probes next
Whether the candidate connects a testing finding to its security implication, rather than treating "ugly error page" and "information disclosure" as the same category of bug.
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.
For a public API, design a policy that decides what error detail is safe to return to CLIENTS versus what stays only in internal logs. Include examples of safe client-facing error formats, how to include a correlation id without leaking internals, and whether/when to include a stack trace in a log versus an API response. Propose an automated test that ensures no sensitive field ever leaks into a client-facing response.
Sample Answer
Direct answer
Decide what error detail reaches a client by defaulting to the minimum that's actually actionable for THAT client (a correlation id and a stable error code, always; a human-readable message only if it's genuinely safe and useful; never a stack trace or internal identifiers), keeping the full detail in internal logs correlated by the same id.
Structured elaboration
- Safe client-facing format:
{error_code, message, correlation_id}at minimum; themessageshould describe what went wrong from the CLIENT's perspective ("the email field is required") never from the server's internal perspective ("NullPointerException in UserValidator.java line 42"). - Correlation ids without leaking internals: a correlation id is safe to expose (it's an opaque token, not information about your system) and is exactly what lets support/engineering find the FULL internal detail later, without the client ever seeing that detail directly.
- Localized user messages: keep the machine-readable
error_codestable and English-invariant; localize the human-readablemessageseparately based on the client's locale, so client code branching onerror_codenever breaks when message wording/translation changes. - Automated tests for no leakage: a test suite that deliberately triggers every known internal exception type and asserts the CLIENT-FACING response contains none of a blocklist of sensitive patterns (stack trace markers, internal hostnames, SQL fragments, raw exception class names for internal errors) catches this class of leak before it ships, since manual review alone reliably misses it under time pressure.
Worked example
An internal psycopg2.OperationalError: could not connect to server: Connection refused... host "10.2.4.19" must never reach a client; the sanitized response is {"error_code": "internal_error", "message": "Something went wrong on our end. Please try again.", "correlation_id": "7f3e-9c"}, while the full raw exception (including the internal hostname) is logged server-side, findable by an engineer searching for correlation_id: 7f3e-9c.
Trade-offs and pitfalls
The hardest cases are 5xx errors that ARE genuinely useful for the client to know more about (a specific downstream service being down, which the client's own retry logic might want to know about specifically); resist the urge to pass through the raw exception message even here, and instead define a small, deliberate set of STRUCTURED, safe detail fields ({"error_code": "dependency_unavailable", "dependency": "payment_gateway"}) rather than either a blanket generic message or a raw leak.
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.