Skip to content

Lesson 0.7.4 — Defensive Programming ​

Bài 0.7.4 — Defensive Programming
Input validation, Guard clause & Boundary checking • 38 phút
0:00 / 0:00

0. Metadata ​

FieldValue
Stage0 — JavaScript Language Foundation
Module0.7 — Error Handling & Code Quality
Lesson0.7.4
CompetencyC01.8 — Error Handling
Depth TargetL2–L3
PrerequisitesGuard Clauses (0.4.4), Try/Catch (0.7.3), Functions (0.5.1), Data Structures (0.6.x)
Estimated Cognitive LoadMedium

1. Why This Exists (Vì sao cần học) ​

Bạn đã biết try/catch để bắt lỗi sau khi nó xảy ra. Nhưng engineer giỏi không chỉ xử lý lỗi — họ ngăn lỗi trước khi nó xảy ra.

Xét đoạn code:

js
function getUserName(user) {
  return user.profile.name;
}

Khi user là null, nó throw TypeError. Bạn có thể try/catch ở nơi gọi. Nhưng tại sao không hỏi từ đầu: "Liệu user có thể là null không?"

Vấn đề cốt lõi

Defensive programming là thói quen viết code giả định rằng input có thể sai, API có thể trả về dữ liệu thiếu, và người dùng có thể làm điều bất ngờ. Nó không phải "nghi ngờ mọi thứ" — nó là kiểm tra giả định ở ranh giới (boundary) để lỗi không lan truyền sâu vào hệ thống.

Bài này dạy bạn:

  • Validate input trước khi xử lý.
  • Dùng guard clause để dừng sớm khi dữ liệu không hợp lệ.
  • Check boundary (array index, number range) trước khi access.
  • Phân biệt "lỗi lập trình" (bug) với "lỗi dữ liệu" (expected invalid input).

Đây là bước chuyển từ "code chạy đúng với input đúng" sang "code chạy ổn với input thực tế".

2. Prerequisites (Yêu cầu đầu vào) ​

Trước khi học bài này, bạn cần:

  • Biết guard clause và early return (0.4.4).
  • Biết if, typeof, ===, !== (0.4.x).
  • Biết try/catch để bắt lỗi (0.7.3).
  • Biết cách truy cập object/array và nhận diện undefined/null (0.6.x).
  • Hiểu throw để báo lỗi sớm (0.7.2).

WARNING

Nếu bạn chưa chắc tại sao if (!user) return; tốt hơn if (user) { ... 20 dòng ... }, quay lại 0.4.4. Guard clause là nền tảng trực tiếp của defensive programming.

3. Learning Objectives (Mục tiêu học tập) ​

Sau bài này, bạn có thể:

  1. Viết guard clause để validate function input.
  2. Kiểm tra boundary trước khi access array index hoặc object nested property.
  3. Phân biệt validation (dữ liệu không hợp lệ) và assertion (lỗi logic không nên xảy ra).
  4. Sử dụng default value hợp lý để xử lý missing data.
  5. Nhận diện "happy path assumption" — giả định input luôn đúng.
  6. Kết hợp defensive check với throw hoặc early return để fail fast.

4. Mental Model (Mô hình tư duy) ​

Mental Model

Defensive Programming = Đặt gác ở cửa ngõ, không để kẻ lạ vào phòng trong

  Input vào function
       ↓
  [Guard 1] Kiểu đúng chưa?
       ↓ Không → Return/Throw ngay
  [Guard 2] Shape đúng chưa?
       ↓ Không → Return/Throw ngay
  [Guard 3] Boundary hợp lý chưa?
       ↓ Không → Return/Throw ngay
       ↓ Có
  Xử lý business logic (happy path)

Boundary Check
  array[index]  → index >= 0 && index < array.length?
  obj.nested.x  → obj && obj.nested?
  number range  → n >= min && n <= max?

Fail Fast
  Phát hiện lỗi ở cửa ngõ → dễ debug
  Để lỗi chạy sâu 5 tầng function → khó trace

Quy tắc vàng:

  • Validate ở boundary — nơi code của bạn tiếp xúc với thế giới bên ngoài (API, user input, file, argument).
  • Fail fast — dừng ngay khi phát hiện bất thường, không để lỗi lan truyền.
  • Guard clause > nested if — kiểm tra và return/throw sớm, không lồng logic vào trong if.
  • Default value cho phép missing — options = options || {} hoặc destructuring default.
  • Đừng defensive quá mức — kiểm tra typeof trong mọi hàm nội bộ là overkill. Chỉ guard ở public API boundary.

5. Core Concepts (Các khái niệm cốt lõi) ​

Essential (Bắt buộc) ​

ConceptÝ nghĩa
Input ValidationKiểm tra argument trước khi xử lý: kiểu, shape, range.
Guard Clauseif (!valid) return/throw; — dừng sớm, tránh lồng nhau.
Null HandlingXử lý null/undefined trước khi access property.
Boundary CheckingKiểm tra index hợp lệ, number trong range, string không rỗng.
Fail FastBáo lỗi ngay tại nơi phát hiện, không để lan truyền.
Default ValueCung cấp fallback khi input thiếu: `config = config

Supporting (Hỗ trợ) ​

ConceptÝ nghĩa
Assertionconsole.assert hoặc explicit check cho điều kiện "không bao giờ sai". Khác với validation — assertion failure = bug.
Destructuring defaultfunction fn({ name = "guest" }) — declarative default.

Awareness (Biết tồn tại) ​

ConceptLý do chưa đào sâu
Runtime validation library (Zod, Joi)Dùng schema để validate phức tạp. Thuộc Stage 6/9.
Design by ContractFormal pre/post-condition. Ngoài phạm vi Stage 0.

Out of Scope (Không thuộc bài này) ​

  • TypeScript static type checking (Stage 6).
  • Schema validation phức tạp (Stage 9).
  • Async input validation (Stage 3).

6. Worked Example (Ví dụ phân tích từng bước) ​

Bài toán: Viết function getDisplayName an toàn với mọi input.

js
function getDisplayName(user) {
  return user.profile.name || user.email || "Anonymous";
}

Vấn đề: user có thể là null. user.profile có thể là undefined. user.profile.name có thể là "" (falsy nhưng hợp lệ).

js
function getDisplayName(user) {
  return user.profile.name || user.email || "Anonymous";
}
// getDisplayName(null) → TypeError
// getDisplayName({ email: "a@b.com" }) → TypeError (profile undefined)
js
function getDisplayName(user) {
  if (!user || typeof user !== "object") {
    return "Anonymous";
  }
  const name = user.profile && user.profile.name;
  if (typeof name === "string" && name.length > 0) {
    return name;
  }
  if (typeof user.email === "string" && user.email.length > 0) {
    return user.email;
  }
  return "Anonymous";
}
js
function getDisplayName(user) {
  if (!user || typeof user !== "object") return "Anonymous";
  
  const profile = user.profile;
  if (profile && typeof profile.name === "string" && profile.name.length > 0) {
    return profile.name;
  }
  if (typeof user.email === "string" && user.email.length > 0) {
    return user.email;
  }
  return "Anonymous";
}

Walkthrough

Step 1 — Kiểm tra top-levelif (!user || typeof user !== "object") — nếu user là null, undefined, string, number → return "Anonymous" ngay. Không để lỗi lan truyền vào .profile.

Step 2 — Null-safe accessconst profile = user.profile; — lấy giá trị. Nếu profile là undefined (do user không có field này), check profile && ... sẽ short-circuit. Không access .name trên undefined.

Step 3 — Boundary checktypeof profile.name === "string" && profile.name.length > 0 — đảm bảo là string và không rỗng. Tại sao không dùng ||? Vì "" là falsy nhưng hợp lệ — nếu business logic chấp nhận tên rỗng thì không nên fallback. Ở đây ta quyết định tên rỗng = không hợp lệ.

Step 4 — Fallback chainprofile.name → user.email → "Anonymous". Mỗi bước đều validated.

Key Insight: Defensive programming không phải "thêm if khắp nơi". Nó là kiểm tra giả định tại ranh giới và cung cấp fallback hợp lý. Code dài hơn một chút nhưng không crash và dễ debug hơn khi có vấn đề.

7. Prediction Exercise (Bài tập dự đoán) ​

Đừng chạy code. Đọc và dự đoán:

  • (1) Output là gì?
  • (2) Có throw không?
  • (3) Nếu viết defensive hơn, nên sửa như thế nào?

Câu 1 ​

js
function getFirst(items) {
  return items[0].toUpperCase();
}
getFirst(["a", "b"]);
getFirst([]);

Câu 2 ​

js
function divide(a, b) {
  return a / b;
}
divide(10, 0);

Câu 3 ​

js
function createUser(data) {
  return {
    name: data.name,
    age: data.age + 1
  };
}
createUser({ name: "Alice" });

Câu 4 ​

js
function formatDate(timestamp) {
  const date = new Date(timestamp);
  return date.toISOString();
}
formatDate("invalid");

Câu 5 ​

js
function pick(obj, key) {
  return obj[key];
}
pick(null, "name");

Câu 6 (Transfer — Phân biệt validation vs assertion) ​

js
function internalHelper(arr) {
  if (!Array.isArray(arr)) throw new Error("Must be array");
  return arr.map(x => x * 2);
}

function publicApi(input) {
  const items = input.split(",").map(Number);
  return internalHelper(items);
}

Câu hỏi:

  1. internalHelper throw Error khi arr không phải array. Đây là validation hay assertion? Tại sao?
  2. Nếu publicApi được gọi với null, lỗi xảy ra ở đâu? Stack trace sẽ chỉ đến publicApi hay internalHelper?
  3. Nên defensive ở publicApi hay internalHelper, hay cả hai?
[Đáp án & Giải thích]

Câu 1:

  • getFirst(["a", "b"]) → "A" ✅
  • getFirst([]) → TypeError: Cannot read properties of undefined (reading 'toUpperCase') ❌
  • Defensive: if (!items || items.length === 0) return ""; return items[0].toUpperCase();

Câu 2:

  • divide(10, 0) → Infinity (không throw). Nhưng domain logic có thể coi đây là lỗi.
  • Defensive: if (b === 0) throw new RangeError("Division by zero"); hoặc return null tùy contract.

Câu 3:

  • createUser({ name: "Alice" }) → { name: "Alice", age: NaN } ❌ (silent failure)
  • Defensive: if (typeof data.age !== "number") throw new TypeError("age required"); hoặc default age: (data.age || 0) + 1 tùy intent.

Câu 4:

  • formatDate("invalid") → Invalid Date, toISOString() throw RangeError: Invalid time value
  • Defensive: if (isNaN(date.getTime())) throw new RangeError("Invalid timestamp");

Câu 5:

  • pick(null, "name") → TypeError: Cannot read properties of null (reading 'name') — wait, obj[key] với null throw vì null không thể property access.
  • Defensive: if (!obj || typeof obj !== "object") return undefined;

Câu 6:

  • 1. Assertion. internalHelper là hàm nội bộ. Nó giả định caller đã validate. Throw ở đây là để phát hiện bug (caller truyền sai), không phải để xử lý input người dùng.
  • 2. Lỗi xảy ra ở publicApi tại input.split(...) vì null.split() throw TypeError. Stack trace chỉ đến publicApi, không đến internalHelper.
  • 3. Cả hai, nhưng khác mục đích:
    • publicApi: Validation — check input là string, handle user input. Đây là "lỗi dự kiến" từ bên ngoài.
    • internalHelper: Assertion — check arr là array để phát hiện bug nội bộ sớm. Đây là "lỗi không nên xảy ra" nếu codebase đúng. Nếu assertion fail, nghĩa là publicApi hoặc caller nội bộ có bug.

Quy tắc: Validation ở boundary (API, user input). Assertion ở nội bộ để phát hiện contract violation. Đừng dùng throw assertion cho user input — message không friendly. Đừng dùng silent validation cho bug nội bộ — sẽ thành silent failure.


Transfer Check ​

Bạn chưa từng gặp đoạn code sau. Dựa vào mental model, hãy trả lời:

js
function processUsers(users, limit) {
  const result = [];
  for (let i = 0; i < limit; i++) {
    result.push(users[i].name.toUpperCase());
  }
  return result;
}
  1. Liệt kê 3 giả định "happy path" mà function này đang ngầm đưa ra.
  2. Nếu users là API response, tại sao 3 giả định này đặc biệt nguy hiểm?
  3. Viết lại function với defensive checks cho cả 3 giả định.
[Đáp án & Giải thích]
  1. 3 giả định:

    • users là array (không phải null/undefined).
    • users[i] tồn tại với mọi i < limit (array đủ dài).
    • users[i].name là string (tồn tại và có .toUpperCase).
  2. Nguy hiểm: API có thể trả về null thay vì array. API có thể trả về array rỗng. API có thể trả về object thiếu name hoặc name là null. Tất cả đều gây crash ở production.

  3. Defensive rewrite:

    js
    function processUsers(users, limit) {
      if (!Array.isArray(users)) return [];
      const safeLimit = Math.min(limit, users.length);
      const result = [];
      for (let i = 0; i < safeLimit; i++) {
        const user = users[i];
        if (user && typeof user.name === "string") {
          result.push(user.name.toUpperCase());
        }
      }
      return result;
    }

Bài học: Mỗi lần bạn viết a.b.c, bạn đang đưa ra 3 giả định: a tồn tại, a.b tồn tại, a.b.c hợp lệ. Defensive programming là việc kiểm tra những giả định đó ở ranh giới.

8. Implementation Lab (Bài lab thực hành) ​

Level 1 — Guided (Có hướng dẫn) ​

Viết function safeGet defensive để lấy property từ object mà không throw.

js
function safeGet(obj, key) {
  if (___________ || typeof obj !== "object") {
    return _________;
  }
  return obj[key];
}

// Test:
console.log(safeGet({ a: 1 }, "a")); // 1
console.log(safeGet(null, "a"));     // undefined
console.log(safeGet({ a: 1 }, "b")); // undefined

Gợi ý

!obj, undefined. Nhớ typeof obj !== "object" để bắt string/number truyền nhầm.

[Đáp án tham khảo]
js
function safeGet(obj, key) {
  if (!obj || typeof obj !== "object") {
    return undefined;
  }
  return obj[key];
}

console.log(safeGet({ a: 1 }, "a")); // 1
console.log(safeGet(null, "a"));     // undefined
console.log(safeGet({ a: 1 }, "b")); // undefined

Giải thích:

  • Guard đầu tiên bắt null, undefined, và non-object.
  • Trả về undefined — một giá trị "falsy" rõ ràng cho phép caller kiểm tra.
  • Không dùng try/catch vì đây là check điều kiện đơn giản, không phải exception handling.

Level 2 — Partial Scaffold (Khung sẵn) ​

Hoàn thành function safeDivide với defensive checks.

js
function safeDivide(a, b, fallback) {
  if (___________ || _________) {
    throw new TypeError("Operands must be numbers");
  }
  if (___________) {
    return _________;
  }
  return a / b;
}

// Test:
console.log(safeDivide(10, 2, 0));   // 5
console.log(safeDivide(10, 0, -1));  // -1
// console.log(safeDivide("10", 2, 0)); // TypeError
[Đáp án tham khảo]
js
function safeDivide(a, b, fallback) {
  if (typeof a !== "number" || typeof b !== "number") {
    throw new TypeError("Operands must be numbers");
  }
  if (b === 0) {
    return fallback;
  }
  return a / b;
}

console.log(safeDivide(10, 2, 0));   // 5
console.log(safeDivide(10, 0, -1));  // -1
// safeDivide("10", 2, 0);
// TypeError: Operands must be numbers

Giải thích:

  • Validate kiểu trước — fail fast nếu caller truyền sai.
  • Check boundary b === 0 — trả về fallback thay vì Infinity.
  • fallback cho phép caller quyết định giá trị mặc định.

Level 3 — Independent (Tự viết) ​

Viết các function sau với defensive programming:

js
// 1. clamp(value, min, max) → Trả về value nếu trong [min, max].
//    Nếu < min, trả về min. Nếu > max, trả về max.
//    Nếu input không phải number, throw TypeError.

// 2. safeSlice(arr, start, end) → Giống arr.slice nhưng defensive.
//    Nếu arr không phải array, trả về [].
//    Nếu start/end vượt boundary, tự điều chỉnh (không throw).
//    Ví dụ: safeSlice(["a", "b", "c"], -1, 10) → ["c"]

// 3. formatUser(user) → user có thể là bất kỳ giá trị nào.
//    Trả về string "{name} ({age})" nếu hợp lệ.
//    Nếu user không phải object, trả về "Unknown".
//    Nếu name missing hoặc không phải string, dùng "Guest".
//    Nếu age missing hoặc không phải number, dùng 0.

// Sử dụng:
console.log(clamp(5, 0, 10));      // 5
console.log(clamp(-5, 0, 10));     // 0
console.log(clamp(15, 0, 10));     // 10
// console.log(clamp("5", 0, 10)); // TypeError

console.log(safeSlice(["a", "b"], 0, 5)); // ["a", "b"]
console.log(safeSlice(null, 0, 2));       // []

console.log(formatUser({ name: "Alice", age: 30 })); // "Alice (30)"
console.log(formatUser(null));                       // "Unknown"
console.log(formatUser({ age: 25 }));                // "Guest (25)"
[Đáp án tham khảo]
js
function clamp(value, min, max) {
  if (typeof value !== "number" || typeof min !== "number" || typeof max !== "number") {
    throw new TypeError("All arguments must be numbers");
  }
  if (value < min) return min;
  if (value > max) return max;
  return value;
}

function safeSlice(arr, start, end) {
  if (!Array.isArray(arr)) return [];
  const len = arr.length;
  const s = Math.max(0, Math.min(start, len));
  const e = Math.max(0, Math.min(end, len));
  return arr.slice(s, e);
}

function formatUser(user) {
  if (!user || typeof user !== "object") return "Unknown";
  const name = typeof user.name === "string" ? user.name : "Guest";
  const age = typeof user.age === "number" ? user.age : 0;
  return `${name} (${age})`;
}

console.log(clamp(5, 0, 10));      // 5
console.log(clamp(-5, 0, 10));     // 0
console.log(clamp(15, 0, 10));     // 10

console.log(safeSlice(["a", "b"], 0, 5)); // ["a", "b"]
console.log(safeSlice(null, 0, 2));       // []

console.log(formatUser({ name: "Alice", age: 30 })); // "Alice (30)"
console.log(formatUser(null));                       // "Unknown"
console.log(formatUser({ age: 25 }));                // "Guest (25)"

Giải thích:

  • clamp: Validate kiểu trước. Boundary check đơn giản.
  • safeSlice: Guard Array.isArray. Điều chỉnh start/end về [0, length] — defensive không phải lúc nào cũng throw, đôi khi là normalize.
  • formatUser: Layered fallback. Object? → Name valid? → Age valid? → Format. Mỗi layer có default.

9. Edge Cases (Các trường hợp ngoại lệ) ​

Defensive quá mức trong internal code

js
function internalSum(a, b) {
  if (typeof a !== "number") throw new TypeError("a must be number");
  if (typeof b !== "number") throw new TypeError("b must be number");
  return a + b;
}

Tại sao: Nếu internalSum chỉ được gọi bởi 1 hàm đã validate, các check này là redundancy. Làm chậm hot path và tăng noise.

Cách nhận biết: Mọi hàm nội bộ đều check typeof — code bị "defensive bloat".

Cách xử lý: Chỉ validate ở public boundary (API entry, user input). Nội bộ dùng assertion (hoặc không check nếu contract rõ ràng).

typeof null === "object" trap

js
function getName(user) {
  if (typeof user === "object") {
    return user.name; // user có thể là null!
  }
}

Tại sao: null qua typeof check nhưng vẫn throw khi access property.

Cách nhận biết: TypeError: Cannot read properties of null dù đã check typeof.

Cách xử lý: Luôn kết hợp truthy check: if (user && typeof user === "object").

Falsy nhưng hợp lệ

js
function setCount(count) {
  if (!count) count = 10; // Bug!
  return count;
}
setCount(0); // 10 ❌

Tại sao: 0 là falsy nhưng là giá trị hợp lệ. !0 là true nên count bị ghi đè thành 10. Guard bằng !value không phân biệt được 0 (hợp lệ) với null/undefined (không hợp lệ).

Cách nhận biết: Input 0, "", false bị override không đúng ý.

Cách xử lý: Dùng explicit check kiểu hoặc nullish:

js
if (typeof count !== "number") count = 10;
// hoặc
count = count ?? 10; // nullish coalescing — chỉ fallback với null/undefined

Default object và reference sharing

js
const DEFAULT_CONFIG = { host: "localhost", debug: false };

function setup(config = DEFAULT_CONFIG) {
  config.port = config.port || 3000;
  return config;
}

const a = setup();
const b = setup({ port: 8080 });
console.log(a.port); // 8080 ❌ — `a` bị đổi theo vì `DEFAULT_CONFIG` là shared reference

Tại sao: Khi default value là một biến/reference bên ngoài (không phải literal như [] hay {}), nó được share giữa các call. Mọi mutation đều ảnh hưởng lần gọi sau.

Cách nhận biết: Kết quả của lần gọi trước bất ngờ ảnh hưởng lần gọi sau.

Cách xử lý: Luôn dùng literal trong default parameter (config = {}), hoặc clone nếu cần merge:

js
function setup(config) {
  config = config || {};
  config.port = config.port || 3000;
  return config;
}

Lưu ý quan trọng: function f(items = []) hoặc function f(options = {}) trong ES6+ là an toàn — engine tạo object/array mới mỗi lần gọi. Chỉ khi bạn gán một biến ngoài (const X = []; function f(a = X)) mới xảy ra sharing.

10. Debug Lab (Bài lab gỡ lỗi) ​

Symptom (Triệu chứng): Function trả về kết quả sai nhưng không throw.

js
function calculateTotal(items, taxRate) {
  let total = 0;
  for (const item of items) {
    total += item.price * item.quantity;
  }
  return total * (1 + taxRate);
}

console.log(calculateTotal([
  { price: 10, quantity: 2 },
  { price: 5, quantity: 1 }
], 0.1));
// 27.5 ✅

console.log(calculateTotal(null, 0.1));
// TypeError: items is not iterable ❌

console.log(calculateTotal([
  { price: "10", quantity: 2 }
], 0.1));
// NaN ❌

Reproduction (Tái hiện lỗi): Chạy với null và với price là string.

Evidence (Bằng chứng):

  • null gây crash — không defensive.
  • price: "10" gây NaN — string * number = NaN, sau đó NaN lan truyền.

Hypothesis (Giả thuyết): Developer giả định items luôn là array và price luôn là number. Không validate input. NaN là silent failure đặc biệt nguy hiểm vì nó "nhiễm" toàn bộ phép tính mà không throw.

Verification (Xác minh):

js
console.log("10" * 2); // 20 (coercion)
console.log("10" * "2"); // 20
console.log("ten" * 2); // NaN

Root Cause (Nguyên nhân gốc rễ): Thiếu validation ở ranh giới. items không được check. item.price không được check là number. JavaScript coercion giấu lỗi kiểu string/number.

Fix (Sửa):

js
function calculateTotal(items, taxRate) {
  if (!Array.isArray(items)) {
    throw new TypeError("items must be an array");
  }
  if (typeof taxRate !== "number") {
    throw new TypeError("taxRate must be a number");
  }
  
  let total = 0;
  for (const item of items) {
    if (!item || typeof item.price !== "number" || typeof item.quantity !== "number") {
      throw new TypeError("Each item must have numeric price and quantity");
    }
    total += item.price * item.quantity;
  }
  return total * (1 + taxRate);
}

Prevention (Phòng ngừa):

  • Hỏi với mỗi parameter: "Giá trị này có thể là gì ngoài happy path?"
  • Đặc biệt cẩn thận với numeric input — JavaScript coercion giấu rất nhiều lỗi.
  • NaN check: sau khi tính toán, if (Number.isNaN(result)) throw ... để phát hiện kết quả hỏng do coercion.
  • Không dùng typeof x === "number" làm đủ — NaN cũng có typeof === "number". Nếu giá trị number phải hợp lệ, thêm !Number.isNaN(x).

11. Design Exercise (Bài tập thiết kế giải pháp) ​

Bạn cần viết hàm getConfig.

js
function getConfig(options) {
  return {
    host: options.host,
    port: options.port,
    debug: options.debug
  };
}
js
function getConfig(options) {
  if (!options || typeof options !== "object") {
    throw new TypeError("Options required");
  }
  if (typeof options.host !== "string") {
    throw new TypeError("host must be string");
  }
  if (typeof options.port !== "number" || options.port < 1 || options.port > 65535) {
    throw new RangeError("port invalid");
  }
  return {
    host: options.host,
    port: options.port,
    debug: !!options.debug
  };
}
js
function getConfig(options) {
  const defaults = { host: "localhost", port: 3000, debug: false };
  const merged = { ...defaults, ...(options || {}) };
  return {
    host: String(merged.host),
    port: Number(merged.port) || 3000,
    debug: Boolean(merged.debug)
  };
}

Câu hỏi:

  1. Option A rủi ro gì khi options là null?
  2. Option B có lợi gì khi đây là public API được gọi bởi nhiều team?
  3. Option C có lợi gì về flexibility? Rủi ro gì khi options.port là "abc"?
  4. Nếu đây là internal utility chỉ gọi bởi 1 module đã validate, bạn chọn option nào?
[Đáp án tham khảo]
  • Bạn nghĩ:

    1. Option A: options là null → TypeError ngay tại options.host. Crash sớm nhưng message không rõ ràng. Không fail fast có kiểm soát.
    2. Option B: Fail fast với message rõ ràng. Caller biết ngay field nào sai. Phù hợp public API — contract rõ ràng.
    3. Option C: Flexible, không throw. Nhưng "abc" → Number("abc") là NaN → NaN || 3000 là 3000. Silent normalization có thể che giấu bug caller.
    4. Internal utility → Option A hoặc lightweight Option B. Không cần validate từng field nếu caller đã có contract.
  • Kết luận: Defensive programming không phải "validate mọi thứ mọi lúc". Nó là "validate ở ranh giới phù hợp với risk level". Public API = strict. Internal = assertion hoặc trust.

12. Production Scenario (Tình huống thực tế) ​

Context: Bạn đang viết một parser cho CSV upload.

js
function parseCSV(lines) {
  const headers = lines[0].split(",");
  const rows = [];
  for (let i = 1; i < lines.length; i++) {
    const values = lines[i].split(",");
    const row = {};
    for (let j = 0; j < headers.length; j++) {
      row[headers[j]] = values[j].trim();
    }
    rows.push(row);
  }
  return rows;
}

Symptom: Với file CSV từ user, app crash hoặc tạo ra object có undefined key.

Constraint: File CSV có thể: rỗng, có dòng trống, thiếu cột, thừa cột.

Câu hỏi:

  1. Liệt kê 3 giả định happy path mà parseCSV đang mắc phải.
  2. Viết lại với defensive checks cho từng giả định.
  3. Nên throw hay return [] khi input là null?
[Đáp án tham khảo]
  • Bạn nghĩ:

    1. 3 giả định:
      • lines là array không rỗng.
      • lines[0] tồn tại và là string.
      • Mỗi dòng có đúng số cột với header.
    2. js
      function parseCSV(lines) {
        if (!Array.isArray(lines) || lines.length === 0) return [];
        const headers = (lines[0] || "").split(",").map(h => h.trim()).filter(Boolean);
        if (headers.length === 0) return [];
        
        const rows = [];
        for (let i = 1; i < lines.length; i++) {
          const line = lines[i];
          if (typeof line !== "string" || line.trim() === "") continue;
          
          const values = line.split(",");
          const row = {};
          for (let j = 0; j < headers.length; j++) {
            row[headers[j]] = values[j] ? values[j].trim() : "";
          }
          rows.push(row);
        }
        return rows;
      }
    3. return [] phù hợp hơn vì "không có data" là một kết quả hợp lệ, không phải lỗi chương trình. Throw chỉ khi input rõ ràng vi phạm contract (ví dụ: caller truyền null nhưng contract yêu cầu array).
  • Bài học: User input là nguồn defensive quan trọng nhất. Mọi giả định về "format chuẩn" đều có thể bị vi phạm.

13. AI-assisted Exercise (Bài tập với AI) ​

Level B — Challenge

  1. Tự trả lời trước: Dự đoán output và vấn đề:
    js
    function getLength(str) {
      if (str) {
        return str.length;
      }
      return 0;
    }
    console.log(getLength(""));
    console.log(getLength("hello"));
    console.log(getLength(null));
  2. Hỏi AI: "Function này có defensive không? Tại sao getLength("") trả về 0 thay vì 0? À, nó trả về 0 nhưng vì lý do đúng hay sai?"
  3. So sánh câu trả lời AI với nhận định của bạn. AI có nhấn mạnh rằng if (str) không phải là check kiểu và "" là falsy nhưng hợp lệ?
  4. Verify bằng MDN: tìm "Truthy" trên MDN, đọc phần các giá trị falsy.

Gợi ý

AI thường biết "" là falsy nhưng đôi khi giải thích mơ hồ bằng cách nói "the function handles null". Nếu AI không chỉ ra rằng if (str) bị "false positive" với 0, "", false — tức là nó từ chối input hợp lệ — bạn đã tìm ra điểm mù. AI cũng có thể suggest dùng if (str != null) nhưng không giải thích tại sao.

[Đáp án tham khảo]
  • Bạn nghĩ: getLength("") trả về 0 — đúng về kết quả nhưng sai về lý do. "" là falsy nên rơi vào return 0, không phải vì "".length là 0. Nếu sau này sửa logic thành if (str) return str.toUpperCase(); return ""; thì getLength("") vẫn trả về 0 (hoặc logic khác) vì lý do sai. Đây là coincidental correctness.

  • AI trả lời (typical):

    • The function is somewhat defensive — it checks if str exists before accessing .length.
    • getLength("") returns 0 because empty string is falsy.
    • getLength("hello") returns 5.
    • getLength(null) returns 0 because null is falsy.
  • So sánh: AI thường đúng về behavior nhưng đôi khi không giải thích tại sao điều này nguy hiểm: if (str) trông như "check tồn tại" nhưng thực ra là "check truthiness". Nhiều developer nghĩ if (x) đủ defensive cho mọi trường hợp.

  • Điểm AI nói sai hoặc quá mơ hồ: "The function checks if str exists" — không chính xác. Nó check truthiness, không phải existence. 0, false, "" đều "tồn tại" nhưng bị từ chối. AI hiếm khi nói rõ: "Nếu bạn muốn check null/undefined, dùng str != null hoặc typeof str === 'string'."

  • Kết luận: Nếu bạn chỉ ra được rằng if (str) là truthiness check, không phải type/existence check, và nó từ chối các giá trị falsy hợp lệ, bạn đã hiểu sâu hơn AI. Concept này sẽ quay lại ở Stage 1 (Coercion & Abstract Equality), Stage 6 (TypeScript strict null check), và Stage 8 (React conditional rendering với 0).

14. Teach Back (Dạy lại) ​

Giả sử một junior developer hỏi bạn:

"Em thấy code anh viết nhiều if check lắm. if (!user) return;, if (!Array.isArray(items)) return [];. Sao không tin tưởng input mà phải check hoài? Code nhìn dài hơn."

Hãy giải thích trong 2 phút, dùng đúng terminology: boundary, fail fast, guard clause, happy path assumption.

Mô phỏng
  • Bạn nói: Không phải không tin tưởng — mà là không giả định.

Khi bạn viết user.name, bạn đang ngầm nói: "Tôi tin rằng user luôn là object và luôn có name." Nhưng trong production, API có thể trả về null. Database có thể có record thiếu field. Người dùng có thể xóa dữ liệu. Đó gọi là happy path assumption — giả định mọi thứ luôn suôn sẻ.

Guard clause if (!user) return; không phải "nghi ngờ" — nó là kiểm tra ranh giới (boundary check). Giống như bạn không để khách vào nhà mà không kiểm tra họ có phải khách mời không. Nếu không phải, dừng ngay ở cửa (fail fast) thay vì để họ vào phòng rồi mới phát hiện.

Code dài hơn một chút, nhưng bạn đổi lại:

  1. Không crash 3 giờ sáng.
  2. Khi có lỗi, bạn biết ngay ở đâu (gần nguồn) thay vì trace qua 5 file.
  3. Người đọc code biết rõ contract: function này chấp nhận gì, từ chối gì.

💡 Tưởng tượng code không defensive như một ngôi nhà không khóa cửa — có thể tiện (không cần chìa), nhưng bất cứ ai cũng có thể vào và phá hoại. Guard clause là khóa cửa: hơi mất công mở, nhưng bảo vệ được bên trong.

Gợi ý đánh giá bản thân

  • Đồng nghiệp có hiểu tại sao "nhiều if" không phải "thiếu tin tưởng" mà là "kiểm soát giả định" không?
  • Bạn có tránh được việc nói "vì anh cẩn thận" không?
  • Nếu đồng nghiệp hỏi: "Vậy khi nào em KHÔNG cần check?" — bạn trả lời được không? (Gợi ý: internal function được gọi bởi code bạn kiểm soát, đã validate ở trên.)
  • Nếu đồng nghiệp viết if (x) để check string — bạn giải thích được tại sao nên dùng typeof không?

15. Assessment (Đánh giá) ​

Objective (Mục tiêu)Hình thức đánh giáTask
Guard clauseImplementation (Thực hành)Implementation Lab Level 1–3
Null handlingPrediction (Dự đoán)Prediction Câu 1, 5
Boundary checkImplementation (Thực hành)Implementation Lab Level 3
Fail fastDebug LabDebug Lab Section
Validation vs assertionPrediction (Dự đoán)Prediction Câu 6 (Transfer)
Default valueImplementation (Thực hành)Implementation Lab Level 3
Phân biệt defensive levelsDesign ExerciseDesign Exercise Section
Explain happy path assumptionTeach BackTeach Back Section

16. Exit Criteria (Tiêu chí qua bài) ​

  • [ ] Có thể viết guard clause để validate function input và dừng sớm.
  • [ ] Có thể kiểm tra boundary trước khi access array index hoặc object nested property.
  • [ ] Có thể phân biệt validation (expected invalid input) và assertion (unexpected bug).
  • [ ] Có thể sử dụng default value hợp lý để xử lý missing data.
  • [ ] Có thể nhận diện happy path assumption trong code và viết lại defensive.
  • [ ] Có thể kết hợp defensive check với throw hoặc early return để fail fast.
  • [ ] Có thể debug lỗi do thiếu validation gây silent failure (ví dụ: NaN từ string input).
  • [ ] Có thể quyết định mức độ defensive phù hợp cho public API vs internal utility.

17. Spiral Connection (Liên kết xoắn ốc) ​

Previous (Trước): Try/Catch (0.7.3) — bạn đã biết bắt lỗi sau khi xảy ra. Giờ bạn học cách ngăn lỗi trước khi nó xảy ra bằng validation và guard clause.

Current (Hiện tại): Defensive Programming — input validation, guard clause, null handling, boundary checking, fail fast. Phân biệt validation vs assertion.

Next (Tiếp theo):

  • 0.7.5 (Readable JavaScript) — Defensive code phải vẫn readable. Quá nhiều guard có thể làm code khó đọc. Cần balance giữa safety và clarity.
  • Stage 1 (Execution Model) — Scope và variable lookup. Tại sao typeof undeclared là "undefined" nhưng truy cập trực tiếp là ReferenceError? Điều này ảnh hưởng cách defensive check biến.
  • Stage 3 (Async) — Async input validation. Race condition và stale data — defensive không chỉ là check kiểu mà còn là check state.
  • Stage 6 (TypeScript) — Static type checking là một lớp defensive compile-time. TypeScript catch nhiều lỗi mà JS runtime mới phát hiện.
  • Stage 8 (React) — Props validation. propTypes hoặc TypeScript props là defensive ở component boundary.
  • Stage 9/13 (Production) — Runtime validation libraries (Zod, Joi). Schema validation cho API contract. Defensive ở system boundary.
📴 Offline Mode — Content served from cache