Error Handling and Defensive Programming Questions
Making code robust against bad input and failure: exceptions versus error returns, input validation, guard clauses, graceful degradation, and designing for the unhappy path. Covers where to handle versus propagate errors and how to fail safely without hiding bugs. A recurring probe of production maturity.
Design an SLO-based alerting strategy that minimizes pager/alert fatigue: what metrics feed the SLO, symptom alerts versus cause alerts, using an error budget to gate alerting, minimum sample sizes, and grouping/sampling strategies for a noisy downstream integration that would otherwise drown out real signals. Sketch a PromQL-like expression for 'error ratio exceeds 1% over 5 minutes with at least 1000 requests'.
Sample Answer
Direct answer
Build alerting on SLOs (not raw error counts) so pages correlate with actual user-facing pain: use an error BUDGET to gate how aggressively you alert, distinguish symptom alerts (something is actually broken for users right now) from cause alerts (a likely contributing factor, lower urgency), and require a minimum sample size before firing so low-traffic noise doesn't page anyone.
Structured elaboration
- Metrics feeding the SLO: typically a ratio (successful requests / total requests) measured against an explicit target (99.9% success over a rolling window), NOT a raw error count, since raw counts don't distinguish '10 errors out of 100 requests' (10% error rate, bad) from '10 errors out of 1,000,000' (negligible).
- Symptom vs cause alerts: a symptom alert ('user-facing error rate exceeds SLO threshold') should page immediately, since it directly reflects user pain; a cause alert ('CPU usage is elevated') is a likely contributing factor but not itself proof of user impact, and should typically be visible on a dashboard or trigger a lower-urgency notification rather than a page, since elevated CPU alone doesn't necessarily mean anyone is affected.
- Error budgets gating alerts: track how much of the SLO's allowed error budget has been consumed over the current period; alert more aggressively (shorter time windows, lower thresholds) as the budget depletes, since burning through budget fast (a 'fast burn') threatens the SLO commitment much sooner than a slow, steady trickle of errors within otherwise-normal bounds.
- Minimum sample size:
error_ratio > 1%computed over 5 requests is meaningless noise; requiring a minimum request count (e.g. 1000) before evaluating the ratio prevents low-traffic periods (overnight, for a low-volume service) from producing statistically meaningless alerts. - Grouping/sampling for a noisy downstream integration: a specific downstream integration that is chronically flaky in a way that is well understood and already tolerated (a third-party API with a known baseline 2% error rate that the product already accounts for) will otherwise dominate a shared SLO's error budget and drown out signal from everything else calling into the same service; carve that dependency's calls into their OWN separate SLO/alert group (or exclude its known-tolerated error class from the primary SLO's numerator entirely) and sample its errors at a lower rate for logging/tracing purposes than a genuinely unexpected error type, so the noisy-but-understood integration doesn't consume alert budget or storage that a rare, unexpected failure needs.
- Combining static thresholds with anomaly detection: a static threshold (error ratio > 1%) is simple and predictable but can't distinguish 'normal daily variance' from a genuine anomaly for a service whose baseline error rate itself fluctuates; layering an anomaly-detection signal (current rate significantly deviates from the same time-of-day/day-of-week historical baseline) catches issues a fixed threshold misses without needing to hand-tune a threshold per time-of-day.
Worked example
PromQL-style expression: sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) > 0.01 and sum(rate(http_requests_total[5m])) * 300 > 1000 fires only when the 5-minute error ratio exceeds 1% AND the 5-minute window saw at least 1000 total requests, avoiding a false alarm during a low-traffic window where a handful of errors could otherwise spike the ratio meaninglessly.
Trade-offs and pitfalls
An SLO-based alert threshold set too close to the actual SLO target itself pages on every minor blip that doesn't genuinely threaten the SLO commitment over its real measurement window; the fast-burn/slow-burn distinction (alert urgently on a fast burn rate that would exhaust the budget in hours, alert less urgently on a slow burn that would exhaust it over weeks) is what lets you page appropriately without either missing a real emergency or drowning on-call in noise from ordinary variance.
You're designing structured error logging for a service (a REST API, or an ML inference endpoint). An on-call engineer investigating an incident should be able to go from one error log line to a full picture of what happened and to whom, without needing to reproduce the bug. Design the log entry's field set to make that possible, and explain why each field you chose earns its place. Contrast a plain stdout print with a structured JSON logging framework, and describe how you would avoid logging sensitive PII while preserving diagnostic value.
Sample Answer
Direct answer
A good error log entry needs enough fields to reconstruct WHAT happened, WHERE, WHEN, and to WHOM, without needing to reproduce the bug: a timestamp, the service and version, a correlation/trace id, the error type and message, severity, and enough context (which user/request) to investigate, while explicitly excluding PII from the message body.
Structured elaboration
- Minimum fields:
timestamp,service,version/deploy_id,correlation_id/trace_id(ties this log line to the specific request and to distributed traces),error_type,message,severity, and acontextmap for anything structured (which endpoint, which downstream call). - Why each matters:
correlation_idis what lets you find EVERY log line for one failing request across multiple services, which is the single most valuable field for incident investigation;service+versiontells you whether a recent deploy correlates with the failure;error_type(not just the message string) lets you aggregate and count occurrences of the SAME underlying bug even if the message includes variable data. - stdout print vs structured logging: a bare
print()produces unstructured text that a log pipeline can only full-text search; a structured JSON logger produces queryable, aggregatable fields, letting you ask 'how manyValidationErrors in the last hour, broken down by field' instead of grepping raw text. - Avoiding PII: never put raw user-identifying data (email, name, full request body) directly in the message string; reference it by an opaque id (
user_id) that can be looked up separately by someone with appropriate access, and redact or hash anything that could be sensitive before it's logged at all.
Worked example
{"timestamp": "...", "level": "ERROR", "service": "checkout-api", "version": "v2.14.1", "correlation_id": "7f3e-9c", "error_type": "PaymentGatewayTimeout", "message": "payment gateway did not respond within 5s", "context": {"user_id": "u_88213", "order_id": "o_44210"}}
A log query for correlation_id: 7f3e-9c across every service instantly reconstructs the full cross-service story of this one failing request; a query for error_type: PaymentGatewayTimeout over the last hour tells you if this is a one-off or a sustained outage, without anyone needing to grep raw text.
Trade-offs and pitfalls
Over-logging (dumping the full request/response body into every error log 'just in case') both risks leaking PII and bloats log storage/cost dramatically at scale; log the minimum structured context that's actually useful for triage, and reference larger payloads by id (stored elsewhere, access-controlled) rather than inlining them. Under-logging (a bare message string with no correlation id) is the more common failure in practice, and it's the one that turns a five-minute investigation into a multi-hour one.
Propose a minimal set of error/reliability metrics every service (or model-serving system) should emit: counters, gauges, and histograms (error_rate_total, retry_count, latency buckets, schema_mismatch_total). Discuss how to label them (service, region, endpoint, model_version) while avoiding high-cardinality labels, and how you would tie these metrics to an experiment or A/B test that proves a specific error-handling change actually improved customer-facing outcomes (error rate, mean time to recovery, retries per request).
Sample Answer
Direct answer
Every service should emit a small, consistent set of error/reliability metrics (an error-rate counter, a retry counter, a latency histogram) labeled by dimensions an operator actually needs to slice by (service, region, endpoint, and for ML systems, model version), while explicitly avoiding high-cardinality labels that would blow up the metrics backend's storage and query cost.
Structured elaboration
- Minimal metric set:
errors_total(counter, labeled by error type/category),retries_total(counter),request_latency_seconds(histogram, for percentile calculations),in_flight_requests(gauge: unlike a counter, a gauge can go down as well as up, which is exactly what's needed to see current concurrent load rather than a cumulative total), and for a model-serving system specifically,prediction_errors_total,model_version_errors_total, andschema_mismatch_total(counter, incremented when an inference request's input fails schema validation before ever reaching the model). - Labeling:
service,region,endpoint(orroute), and (for ML)model_versionare all LOW-cardinality (bounded, small set of possible values), safe to use as labels;user_idor a raw error MESSAGE string are high-cardinality (essentially unbounded distinct values) and must never be used as a label, since most time-series metrics backends create a new time series PER unique label combination, and a high-cardinality label can multiply your storage/query cost by orders of magnitude, sometimes taking down the metrics system itself. - Aggregating into dashboards and SLOs: the error-rate counter divided by a total-requests counter, both labeled consistently by service/region/endpoint, is what directly feeds the SLO-based alerting described in the companion survivor; dashboards typically show error rate over time, sliced by the SAME small set of labels, letting an operator drill from 'overall error rate is elevated' to 'specifically in region=us-east, endpoint=/checkout' without needing a different, ad hoc query each time.
- Example metric names/labels:
http_requests_errors_total{service="checkout-api", region="us-east-1", endpoint="/checkout", error_type="payment_timeout"};model_inference_errors_total{service="reco-model", model_version="v7", error_type="schema_mismatch"}.
Tying metrics to an experiment proving an error-handling change helped
Ship the error-handling change (say, a new retry-with-backoff policy) behind an A/B flag rather than a global rollout, and split traffic randomly between old and new policy while holding everything else constant; then compare the SAME three customer-facing metrics named in canonical_text between arms over the experiment's duration: errors_total / requests_total (did the treatment arm's error rate actually drop, not just its retry count), mean time to recovery per incident-equivalent event (did a transient failure resolve faster for the treatment arm, e.g. time from first error to first subsequent success for the same logical request), and retries_total / requests_total (did the treatment arm need meaningfully fewer retries per request, or did the new policy just retry more aggressively without improving the underlying success rate, which would be a false win). A statistically significant improvement in error rate and MTTR, without a corresponding increase in retries-per-request that merely masks the same underlying failure rate behind more attempts, is what actually proves the change helped rather than just changed behavior.
Trade-offs and pitfalls
The most common and most damaging mistake is putting something UNBOUNDED (a user id, a raw exception message, a request id) directly into a label instead of a fixed, small error_type category derived from it; this single mistake has, in real incidents, caused a metrics system's own storage or query latency to degrade badly enough to become its own outage, layered on top of whatever the original problem was. Keep genuinely high-cardinality detail (which specific user, which specific request) in structured LOGS (searchable, but not a time-series dimension), and keep metrics strictly to bounded, aggregatable dimensions.
Design a crash-consistent, resumable checkpointing scheme for a long-running training job or model artifact: atomic writes (write to a temp file/key then rename), checkpoint frequency trade-offs, lease-based locking so only one exclusive writer runs at a time, safe concurrent reads while a writer is active, backward-compatible artifact format with manifest/version files and checksums for integrity, and a rollback plan if checkpoint verification fails. Explain how you avoid duplicate downstream side effects (metrics, DB writes) when a job resumes after a retry.
Sample Answer
Direct answer
A crash-consistent, resumable checkpointing scheme writes checkpoints atomically (temp file then rename, or content-addressable storage with a checksum), uses a lease-based lock so only one writer holds exclusive write access at a time, and keeps a manifest/version file that a reader consults to know which checkpoint is the current, verified-complete one, with rollback available if a new checkpoint fails verification.
Structured elaboration
-
Atomic writes: write the checkpoint's full content to a temp path, then atomically rename it into place (or, for object storage, use its own atomic-put/versioning semantics); a reader can never observe a partially-written checkpoint this way, since the rename (or object-store put) is the single atomic operation that makes the new version visible.
-
Checkpoint frequency trade-offs: more frequent checkpoints bound the redo-work window on a crash but cost more I/O overhead; base the frequency on measured per-checkpoint write time relative to total training step time, keeping checkpoint overhead to a small, deliberately-bounded percentage of total wall-clock time.
-
Lease-based locking for exclusive writers: acquire a lease (a lock with an expiry) before writing a new checkpoint, so a stuck or crashed writer's lock doesn't block all future writes forever; a lease that isn't renewed within its TTL is assumed abandoned and can be reclaimed by a new writer.
-
Manifest/version files: a small, separately-written file (itself written atomically, AFTER the actual checkpoint data is fully and verifiably written) that names the current valid checkpoint's location and a checksum; a reader trusts ONLY what the manifest points to, never inferring the latest checkpoint from a directory listing, which could show a partially-written one.
-
Rollback on verification failure: after writing a new checkpoint, verify it (checksum match, or a quick load-and-sanity-check) BEFORE updating the manifest to point at it; if verification fails, the manifest still points at the PREVIOUS good checkpoint, and the job can roll back to that automatically rather than resuming from corrupted state.
-
Avoiding duplicate downstream side effects on resume: a resumed job re-executes from the last verified checkpoint, which means any downstream side effect (a metrics emission, a DB write) that happened AFTER that checkpoint but BEFORE the crash gets re-executed a second time on resume; make every downstream side effect idempotent, keyed by a deterministic identifier derived from the checkpoint/step number (a DB write done as an upsert keyed on
job_id, step, a metric emission tagged with the step number so a downstream deduplication window can drop the replay) rather than a blind append/increment that would double-count on replay.
Worked example
A training job checkpoints every 1000 steps: write model+optimizer state to a temp path, compute and verify a checksum of the written file, THEN atomically write an updated manifest ({"latest": "checkpoint_4000.pt", "checksum": "...", "verified": true}) via its own temp-then-rename. If the process crashes DURING the checkpoint write (before the manifest update), the manifest still points atcheckpoint_3000.pt(the last fully-verified one), and resuming the job reads the manifest, findscheckpoint_3000.pt, and redoes at most 1000 steps of work, never touching the partially-writtencheckpoint_4000.ptfile at all. The same resumed run also re-emits the training-progress metric and re-attempts the epoch-summary DB write for steps 3000-4000 that had already been recorded before the crash; both are idempotent (the metric is deduplicated by step number, the DB write is an upsert keyed onjob_id, step), so the replay doesn't double-count progress or duplicate the summary row.
Trade-offs and pitfalls
The manifest update MUST happen strictly after the checkpoint data write is confirmed durable (not just 'the write() call returned', which on many filesystems doesn't guarantee the data has actually reached disk without an explicit fsync); skipping the fsync before the rename is a subtle bug that can pass every normal test (since data usually does reach disk quickly) and only manifests as data loss during an actual crash at exactly the wrong moment, which is precisely the scenario the whole scheme exists to protect against.
You're leading a team with recurring bugs caused by poor error handling and sparse tests (or balancing shipping new features against investing time in defensive engineering). How would you introduce team-level practices to improve this over a quarter: code-review rules, linters, templates, testing quotas, and a phased rollout that gets buy-in from product? Describe your prioritization framework and how you'd measure success.
Sample Answer
Direct answer
Introduce team-level defensive-coding practices over a quarter through a phased rollout (define the practices, socialize and get buy-in, enforce via review/tooling, measure results) rather than a single mandate, since a practice imposed without buy-in or automated enforcement reliably decays back to old habits within weeks.
Structured elaboration
- Phase 1 (weeks 1-2): define and socialize: write down the SPECIFIC practices (not 'write better error handling' but concrete rules: no bare except, every public function validates its inputs, every resource-acquiring block uses a context manager) and discuss them WITH the team, incorporating their pushback, rather than presenting a finished mandate top-down.
- Testing quotas: pair the code-review rules with a lightweight, enforceable minimum (no PR touching error-handling code merges without at least one new test covering the failure path), tracked via a coverage-delta check in CI rather than left to reviewer memory, since 'sparse tests' was named alongside poor error handling as one of the two root problems and needs its own concrete lever, not just an assumption that better error-handling rules will incidentally produce more tests.
- Phase 2 (weeks 3-6): tooling and templates: back the practices with automated enforcement where possible (a lint rule catching the worst offenders) and a PR template/checklist reminding reviewers to check for the rest, so the practice doesn't rely purely on every individual remembering it every time.
- Phase 3 (weeks 7-10): review-driven enforcement: make the practices an explicit part of code review, with the manager (you) modeling the review comments initially so the team sees the calibration (how strict is too strict) rather than each reviewer independently guessing.
- Phase 4 (weeks 11-13): measure and adjust: track a concrete metric (incidents traced to the target failure classes, or a code-quality proxy like lint-rule violation trend) and share the result with the team AND with product, closing the loop on whether the investment paid off.
- Getting buy-in from product: frame the ask in terms product cares about (fewer firefighting-driven schedule disruptions, more predictable delivery) rather than purely as an engineering-quality initiative, and be explicit about the SHORT-TERM velocity cost (code review will be slightly slower initially) versus the medium-term payoff (fewer incidents pulling engineers off roadmap work).
Worked example
Week 1: propose 4 specific rules to the team in a design discussion, incorporating feedback that 2 of the originally-proposed rules were too strict for legacy code paths and should apply to new code only, and agree on the testing quota (one new test per error-handling fix); week 3: ship a lint rule catching bare-except patterns and a CI coverage-delta check enforcing the testing quota, both added as warnings (not yet blocking); week 7: flip the lint rule and the coverage-delta check to blocking for new code, with review-checklist backing for the harder-to-automate rules; week 13: present to the team and to product that incidents in the target category dropped from 3/month to 0/month over the quarter, with review turnaround time increasing by a measured (and acceptable) 10%, framing this as a concrete trade the data supports continuing.
Trade-offs and pitfalls
A rollout that skips the socialization/buy-in phase and goes straight to enforcement generates resentment and passive resistance (reflexive suppression comments, grudging compliance without real behavior change); a rollout that socializes endlessly without ever reaching enforcement never actually changes anything, since good intentions alone don't survive contact with a looming deadline. The phased structure exists specifically to avoid both failure modes.
Unlock Full Question Bank
Get access to all Error Handling and Defensive Programming interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.