Q144: Defensive Programming and Runtime Guards
What Interviewers Want To Evaluate
This question checks whether you can protect JavaScript code at runtime boundaries. Interviewers want assertions, type guards, schema validation, fail-fast behavior, graceful user errors, invariant checks, trust boundaries, and avoiding defensive noise inside trusted code.
Senior answers should explain where runtime guards belong and how they complement TypeScript.
Short Interview Answer
Defensive programming means checking assumptions at boundaries where data or control can be untrusted. Runtime guards validate API responses, URL params, form input, local storage, feature flags, and third-party data. TypeScript helps at compile time, but it cannot prove runtime data is valid. Senior code validates at trust boundaries, fails fast for impossible states, and shows recoverable user errors for expected external failures.
Detailed Interview Answer
JavaScript apps receive data from many places:
API responses
URL params
forms
local storage
feature flags
postMessage
third-party SDKs
configuration
server-rendered payloads
These are trust boundaries. Validate them before the rest of the app relies on them.
TypeScript Is Not Runtime Validation
This compiles:
type User = {
id: string;
name: string;
};
But an API can still return:
{
"id": 42,
"name": null
}
Runtime data must be checked at runtime.
Type Guards
A type guard checks a value and narrows it.
function isUser(value: unknown): value is { id: string; name: string } {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
typeof value.id === "string" &&
typeof value.name === "string"
);
}
For complex shapes, a schema validation library is usually cleaner.
Assertions
Assertions document invariants.
function invariant(condition, message) {
if (!condition) {
throw new Error(message);
}
}
Use assertions for states that should be impossible if the program is correct.
Expected vs Impossible
Separate expected external failures from impossible internal states.
expected: API returns validation error
expected: user is offline
expected: session expires
impossible: selected tab id is not in tab list
impossible: reducer receives unknown action after exhaustive handling
Expected failures need UX. Impossible failures need visibility and fixes.
Fail Fast
Failing fast means throwing or stopping close to the source of invalid state.
This is better than letting bad data travel until it causes a confusing error later.
function getRequiredConfig(config, key) {
const value = config[key];
if (value == null) {
throw new Error(`Missing required config: ${key}`);
}
return value;
}
Graceful Recovery
Not every guard should crash the workflow.
For external data, graceful handling may be better:
show empty state
show validation message
retry request
use fallback config
disable unsupported feature
report error with context
The product decides.
Guard Placement
Put guards near boundaries.
parse API response once
normalize URL params once
validate local storage once
validate form before submit
validate feature config before use
Avoid scattering the same optional checks throughout every component.
Common Mistakes
A common mistake is trusting as SomeType assertions for API data.
const user = response as User;
This only tells TypeScript to trust you. It does not validate anything.
Another mistake is adding defensive checks everywhere instead of normalizing once.
Senior Trade-Offs
Runtime validation has cost. Validate data where it crosses trust boundaries and where failure impact is high.
Inside trusted pure logic, too many defensive checks can reduce readability.
The goal is clear contracts, not fear-driven code.
Code or Design Example
function parsePageParam(value) {
const page = Number(value ?? 1);
if (!Number.isSafeInteger(page) || page < 1) {
return 1;
}
return page;
}
This turns untrusted URL state into safe application state.
Debugging Workflow
When invalid data breaks UI:
identify the trust boundary
inspect raw input
add schema or guard at boundary
normalize to app shape
remove repeated downstream checks
log invalid data with safe context
add tests for malformed input
Interview Framing
Explain trust boundaries and TypeScript limits. Then discuss guards, assertions, fail-fast behavior, and graceful external failure.
Revision Notes
Defensive programming is most valuable at runtime boundaries. Validate once, normalize clearly, and keep impossible states visible.
Key Takeaways
Strong frontend systems do not trust runtime data just because compile-time types look nice.
Follow-Up Questions
- What is a trust boundary?
- Why is TypeScript not runtime validation?
- What is a type guard?
- When should assertions throw?
- What is fail-fast behavior?
- How do expected failures differ from impossible states?
- Where should guards be placed?
- Why is
as Typerisky for API data? - When can defensive checks be excessive?
- How do guards improve debugging?
How I Would Answer This In A Real Interview
I would say runtime guards belong at trust boundaries like APIs, URLs, storage, forms, and third-party data. TypeScript helps after validation, but it cannot validate runtime payloads. I would normalize once and distinguish user-recoverable failures from internal invariants.