Defensive Coding & Validation

Intermediate · 18 min read · ▶ live playground · ✦ checkpoint

Bad input is not an interruption to real programming. It is real programming. Defensive coding means writing code that expects outside data to be messy, checks it at the door, and then lets the rest of the program work with clean values.

Validate at the boundary

Validation is checking that data has the shape and values your program needs. A boundary is any place data enters from the outside: JSON text, a saved file, a form later in Part IV, or a value handed in by another part of the app.

Think of the boundary like the front desk of a clinic. The front desk checks the name, appointment time, and insurance card once. The doctor should not have to re-check the clipboard before every sentence.

function normalizeSignup(rawSignup) {
  if (typeof rawSignup !== "object" || rawSignup === null) {
    throw new TypeError("Signup must be an object");
  }
 
  if (typeof rawSignup.name !== "string" || rawSignup.name.trim() === "") {
    throw new TypeError("Signup name is required");
  }
 
  const seats = rawSignup.seats ?? 1;
 
  if (!Number.isInteger(seats) || seats < 1) {
    throw new RangeError("Seats must be a positive whole number");
  }
 
  return {
    name: rawSignup.name.trim(),
    seats,
  };
}
 
const signups = [
  { name: " Mina ", seats: 2 },
  { name: "", seats: 1 },
  { name: "Rae" },
];
 
for (const rawSignup of signups) {
  try {
    const signup = normalizeSignup(rawSignup);
    console.log(`${signup.name}: ${signup.seats}`);
  } catch (error) {
    console.log(`rejected: ${error.message}`);
  }
}

Mina: 2, then rejected: Signup name is required, then Rae: 1.

normalizeSignup turns unknown input into a clean internal shape. After that, code can trust signup.name is a non-empty string and signup.seats is a positive whole number. That is "validate at the boundary, trust inside."

Guard clauses keep the path flat

A guard clause is an early check that exits before the main work begins. You already met the shape in conditionals: check the locked doors before carrying furniture through the house.

Without guards, code nests deeper every time you add a rule. With guards, the blockers stay at the top and the normal path stays easy to see.

function choosePractice(student) {
  if (typeof student !== "object" || student === null) {
    return "Choose a student first";
  }
 
  if (student.active !== true) {
    return `${student.name} is paused`;
  }
 
  if (!Array.isArray(student.completedLessons) || student.completedLessons.length === 0) {
    return `${student.name}: start with lesson 1`;
  }
 
  const lastLesson = student.completedLessons.at(-1);
  return `${student.name}: review ${lastLesson}`;
}
 
console.log(choosePractice(null));
console.log(choosePractice({ name: "Mina", active: false, completedLessons: ["8.3"] }));
console.log(choosePractice({ name: "Rae", active: true, completedLessons: [] }));
console.log(choosePractice({ name: "Sol", active: true, completedLessons: ["9.1", "9.2"] }));

Choose a student first
Mina is paused
Rae: start with lesson 1
Sol: review 9.2

Guard clauses do not always throw. Use return when the blocker is an expected business case. Use throw when the data is invalid and the function cannot produce an honest result.

Use ?. and ?? for safe reads

Null safety is code that handles null and undefined deliberately instead of crashing by accident. Optional chaining (?.) stops a property read when the value to its left is null or undefined. Nullish coalescing (??) uses a fallback only when the value to its left is null or undefined.

Use them like spare name tags at the front desk: only fill one in when the slot is actually empty.

const reports = [
  { student: { name: "Mina", coach: { name: "Ira" }, streakDays: 0 } },
  { student: { name: "Rae" } },
  { student: null },
];
 
for (const report of reports) {
  const name = report.student?.name ?? "Unknown student";
  const coach = report.student?.coach?.name ?? "No coach";
  const streakDays = report.student?.streakDays ?? 1;
 
  console.log(`${name} | ${coach} | streak ${streakDays}`);
}

Mina | Ira | streak 0
Rae | No coach | streak 1
Unknown student | No coach | streak 1

That first 0 matters. ?? keeps 0, false, and "" because those are real values. It only falls back for null and undefined.

Do not use ?. to hide data you actually require. If a budget record must have label, validate it and throw a clear error. If coach is optional, ?. and ?? are the right tools.

Try it — validate a tiny budget

Run this once. Then fix the broken expense and run it again. The code stores money as whole-number cents, validates at the boundary, and formats dollars only for display.

javascript — live consolereal engine
⌘/Ctrl + Enter to run

The budget fails before any summary prints because one expense has cents as a string. That is deliberate. The program knows exactly what broke and says so.

Fail fast and fail loud

Fail fast means stop at the first point where the program knows the data is invalid. Fail loud means report the problem clearly instead of quietly guessing. Together, they are the smoke alarm from 9.1: annoying for a moment, useful because it points at the fire.

function totalExpenseCents(expenses) {
  if (!Array.isArray(expenses)) {
    throw new TypeError("Expenses must be an array");
  }
 
  return expenses.reduce((total, expense) => {
    if (!Number.isInteger(expense.cents)) {
      throw new TypeError(`${expense.label} cents must be a whole number`);
    }
 
    return total + expense.cents;
  }, 0);
}
 
try {
  console.log(totalExpenseCents([
    { label: "Coffee", cents: 450 },
    { label: "Desk", cents: "3200" },
  ]));
} catch (error) {
  console.log(error.message);
}

Desk cents must be a whole number.

A quiet version might treat "3200" as 0 or skip the row. That keeps the program moving, but now the total is false. Defensive code does not mean "never fail." It means fail where the evidence is fresh and the message can still be specific.

Keep checks near the door

Once boundary validation succeeds, avoid scattering the same checks through every helper. The inside of the program should read like it trusts its own data.

function summarizeValidatedBudget(budget) {
  const totalCents = budget.expenses.reduce((total, expense) => total + expense.cents, 0);
  return `${budget.ownerName} spent ${budget.expenses.length} times: ${totalCents} cents`;
}
 
const budget = {
  ownerName: "Mina",
  expenses: [
    { label: "Coffee", cents: 450 },
    { label: "Notebook", cents: 1200 },
  ],
};
 
console.log(summarizeValidatedBudget(budget));

Mina spent 2 times: 1650 cents.

That helper assumes the boundary already did its job. If every inner function keeps asking "is this really an array?" the program gets noisy and still may miss the one place data first went wrong.

In 9.3, you will move from writing defensive code to investigating code that already misbehaves. The next skill is using DevTools to watch values change instead of guessing from the outside.

Checkpoint

Answer all three to mark this lesson complete

+50 XP on completion