Volume 2

Q144: Defensive Programming and Runtime Guards

Difficulty: SeniorFrequency: HighAnswer time: 8-12 minutes

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

  1. What is a trust boundary?
  2. Why is TypeScript not runtime validation?
  3. What is a type guard?
  4. When should assertions throw?
  5. What is fail-fast behavior?
  6. How do expected failures differ from impossible states?
  7. Where should guards be placed?
  8. Why is as Type risky for API data?
  9. When can defensive checks be excessive?
  10. 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.