Lesson 0.7.4 — Defensive Programming
0. Metadata
| Field | Value |
|---|---|
| Stage | 0 — JavaScript Language Foundation |
| Module | 0.7 — Error Handling & Code Quality |
| Lesson | 0.7.4 |
| Competency | C01.8 — Error Handling |
| Depth Target | L2–L3 |
| Prerequisites | Guard Clauses (0.4.4), Try/Catch (0.7.3), Functions (0.5.1), Data Structures (0.6.x) |
| Estimated Cognitive Load | Medium |
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:
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ể:
- Viết guard clause để validate function input.
- Kiểm tra boundary trước khi access array index hoặc object nested property.
- 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).
- Sử dụng default value hợp lý để xử lý missing data.
- Nhận diện "happy path assumption" — giả định input luôn đúng.
- Kết hợp defensive check với
throwhoặ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ó traceQuy 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
typeoftrong 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 Validation | Kiểm tra argument trước khi xử lý: kiểu, shape, range. |
| Guard Clause | if (!valid) return/throw; — dừng sớm, tránh lồng nhau. |
| Null Handling | Xử lý null/undefined trước khi access property. |
| Boundary Checking | Kiểm tra index hợp lệ, number trong range, string không rỗng. |
| Fail Fast | Báo lỗi ngay tại nơi phát hiện, không để lan truyền. |
| Default Value | Cung cấp fallback khi input thiếu: `config = config |
Supporting (Hỗ trợ)
| Concept | Ý nghĩa |
|---|---|
| Assertion | console.assert hoặc explicit check cho điều kiện "không bao giờ sai". Khác với validation — assertion failure = bug. |
| Destructuring default | function fn({ name = "guest" }) — declarative default. |
Awareness (Biết tồn tại)
| Concept | Lý 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 Contract | Formal 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.
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ệ).
function getDisplayName(user) {
return user.profile.name || user.email || "Anonymous";
}
// getDisplayName(null) → TypeError
// getDisplayName({ email: "a@b.com" }) → TypeError (profile undefined)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";
}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
function getFirst(items) {
return items[0].toUpperCase();
}
getFirst(["a", "b"]);
getFirst([]);Câu 2
function divide(a, b) {
return a / b;
}
divide(10, 0);Câu 3
function createUser(data) {
return {
name: data.name,
age: data.age + 1
};
}
createUser({ name: "Alice" });Câu 4
function formatDate(timestamp) {
const date = new Date(timestamp);
return date.toISOString();
}
formatDate("invalid");Câu 5
function pick(obj, key) {
return obj[key];
}
pick(null, "name");Câu 6 (Transfer — Phân biệt validation vs assertion)
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:
internalHelperthrowErrorkhiarrkhông phải array. Đây là validation hay assertion? Tại sao?- Nếu
publicApiđược gọi vớinull, lỗi xảy ra ở đâu? Stack trace sẽ chỉ đếnpublicApihayinternalHelper? - Nên defensive ở
publicApihayinternalHelper, 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ặcreturn nulltù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 defaultage: (data.age || 0) + 1tùy intent.
Câu 4:
formatDate("invalid")→Invalid Date,toISOString()throwRangeError: 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ớinullthrow vì null không thể property access.- Defensive:
if (!obj || typeof obj !== "object") return undefined;
Câu 6:
- 1. Assertion.
internalHelperlà 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 ở
publicApitạiinput.split(...)vìnull.split()throwTypeError. Stack trace chỉ đếnpublicApi, không đếninternalHelper. - 3. Cả hai, nhưng khác mục đích:
publicApi: Validation — checkinputlà string, handle user input. Đây là "lỗi dự kiến" từ bên ngoài.internalHelper: Assertion — checkarrlà 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àpublicApihoặ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
throwassertion 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:
function processUsers(users, limit) {
const result = [];
for (let i = 0; i < limit; i++) {
result.push(users[i].name.toUpperCase());
}
return result;
}- Liệt kê 3 giả định "happy path" mà function này đang ngầm đưa ra.
- Nếu
userslà API response, tại sao 3 giả định này đặc biệt nguy hiểm? - Viết lại function với defensive checks cho cả 3 giả định.
[Đáp án & Giải thích]
3 giả định:
userslà array (không phảinull/undefined).users[i]tồn tại với mọii < limit(array đủ dài).users[i].namelà string (tồn tại và có.toUpperCase).
Nguy hiểm: API có thể trả về
nullthay vì array. API có thể trả về array rỗng. API có thể trả về object thiếunamehoặcnamelànull. Tất cả đều gây crash ở production.Defensive rewrite:
jsfunction 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.
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")); // undefinedGợi ý
!obj, undefined. Nhớ typeof obj !== "object" để bắt string/number truyền nhầm.
[Đáp án tham khảo]
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")); // undefinedGiả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/catchvì đâ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.
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]
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 numbersGiả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. fallbackcho 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:
// 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]
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: GuardArray.isArray. Điều chỉnhstart/endvề[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
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
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ệ
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:
if (typeof count !== "number") count = 10;
// hoặc
count = count ?? 10; // nullish coalescing — chỉ fallback với null/undefinedDefault object và reference sharing
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 referenceTạ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:
function setup(config) {
config = config || {};
config.port = config.port || 3000;
return config;
}Lưu ý quan trọng:
function f(items = [])hoặcfunction 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.
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):
nullgây crash — không defensive.price: "10"gâyNaN— string * number =NaN, sau đóNaNlan 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):
console.log("10" * 2); // 20 (coercion)
console.log("10" * "2"); // 20
console.log("ten" * 2); // NaNRoot 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):
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.
NaNcheck: 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 đủ —NaNcũ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.
function getConfig(options) {
return {
host: options.host,
port: options.port,
debug: options.debug
};
}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
};
}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:
- Option A rủi ro gì khi
optionslànull? - Option B có lợi gì khi đây là public API được gọi bởi nhiều team?
- Option C có lợi gì về flexibility? Rủi ro gì khi
options.portlà"abc"? - 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ĩ:
- Option A:
optionslànull→TypeErrorngay tạioptions.host. Crash sớm nhưng message không rõ ràng. Không fail fast có kiểm soát. - 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.
- Option C: Flexible, không throw. Nhưng
"abc"→Number("abc")làNaN→NaN || 3000là3000. Silent normalization có thể che giấu bug caller. - Internal utility → Option A hoặc lightweight Option B. Không cần validate từng field nếu caller đã có contract.
- Option A:
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.
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:
- Liệt kê 3 giả định happy path mà
parseCSVđang mắc phải. - Viết lại với defensive checks cho từng giả định.
- Nên
throwhayreturn []khi input lànull?
[Đáp án tham khảo]
Bạn nghĩ:
- 3 giả định:
lineslà 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.
- 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; } 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ềnnullnhưng contract yêu cầu array).
- 3 giả định:
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
- 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)); - 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?" - 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ệ? - 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àoreturn 0, không phải vì"".lengthlà 0. Nếu sau này sửa logic thànhif (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
strexists before accessing.length. getLength("")returns0because empty string is falsy.getLength("hello")returns5.getLength(null)returns0becausenullis falsy.
- The function is somewhat defensive — it checks if
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ùngstr != nullhoặctypeof 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ới0).
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
ifcheck 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:
- Không crash 3 giờ sáng.
- Khi có lỗi, bạn biết ngay ở đâu (gần nguồn) thay vì trace qua 5 file.
- 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ùngtypeofkhông?
15. Assessment (Đánh giá)
| Objective (Mục tiêu) | Hình thức đánh giá | Task |
|---|---|---|
| Guard clause | Implementation (Thực hành) | Implementation Lab Level 1–3 |
| Null handling | Prediction (Dự đoán) | Prediction Câu 1, 5 |
| Boundary check | Implementation (Thực hành) | Implementation Lab Level 3 |
| Fail fast | Debug Lab | Debug Lab Section |
| Validation vs assertion | Prediction (Dự đoán) | Prediction Câu 6 (Transfer) |
| Default value | Implementation (Thực hành) | Implementation Lab Level 3 |
| Phân biệt defensive levels | Design Exercise | Design Exercise Section |
| Explain happy path assumption | Teach Back | Teach 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
throwhoặc early return để fail fast. - [ ] Có thể debug lỗi do thiếu validation gây silent failure (ví dụ:
NaNtừ 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 undeclaredlà"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.
propTypeshoặc TypeScript props là defensive ở component boundary.- Stage 9/13 (Production) — Runtime validation libraries (Zod, Joi). Schema validation cho API contract. Defensive ở system boundary.