Skip to content

Lesson 0.7.2 — Throw ​

Bài 0.7.2 — Throw
throw & Error propagation concept • 33 phút
0:00 / 0:00

0. Metadata ​

FieldValue
Stage0 — JavaScript Language Foundation
Module0.7 — Error Handling & Code Quality
Lesson0.7.2
CompetencyC01.8 — Error Handling
Depth TargetL2–L3
PrerequisitesErrors (0.7.1), Functions (0.5.1), Control Flow (0.4.x)
Estimated Cognitive LoadMedium

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

Bạn đã biết error là một object chứa thông tin. Nhưng error object chỉ ngồi im trong bộ nhớ thì vô dụng. Nó cần được kích hoạt — để dừng luồng hiện tại và báo hiệu cho phần còn lại của chương trình biết rằng "đã có chuyện không ổn".

Xét đoạn code:

js
function divide(a, b) {
  if (b === 0) {
    return null;
  }
  return a / b;
}

const result = divide(10, 0);
console.log(result + 5); // null + 5 = 5 ❌

Vấn đề cốt lõi

Trả về null để báo lỗi là silent failure. Caller không biết đó là lỗi hay kết quả hợp lệ. null + 5 thành 5 — bug lan truyền âm thầm.

throw giải quyết điều này bằng cách:

  1. Dừng ngay lập tức function hiện tại.
  2. Truyền error object lên call stack cho đến khi gặp ai đó xử lý.
  3. Bắt buộc caller phải đối mặt với lỗi — không thể vô tình ignore.

Bài này dạy bạn dùng throw như một cơ chế propagation: không phải để trừng phạt, mà để thông tin lỗi di chuyển đúng hướng trong chương trình.

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

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

  • Biết error là object với message, name, stack (0.7.1).
  • Biết function call và return (0.5.1).
  • Hiểu if và control flow (0.4.2).
  • Biết undefined, null là giá trị hợp lệ có thể bị nhầm với lỗi (0.2.3).

WARNING

Nếu bạn chưa chắc tại sao return null để báo lỗi là nguy hiểm, quay lại 0.7.1. Throw chỉ có ý nghĩa khi bạn hiểu sự khác biệt giữa "trả về giá trị đặc biệt" và "dừng execution với lỗi".

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

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

  1. Giải thích throw dừng execution và truyền value lên call stack.
  2. Sử dụng throw với Error object để báo lỗi rõ ràng.
  3. Dự đoán luồng execution khi gặp throw trong nested function calls.
  4. Phân biệt throw Error object và throw primitive value.
  5. Nhận diện uncaught error và hiểu hậu quả (crash script).
  6. Viết guard clause kết hợp throw để validate input sớm.

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

Mental Model

Throw = Ném "bóng lỗi" lên trên

  throw new Error("fail")
       ↓
  Function hiện tại DỪNG NGAY LẬP TỨC
       ↓
  Bóng lỗi bay lên function gọi nó
       ↓
  Function gọi cũng dừng (trừ khi có try/catch — 0.7.3)
       ↓
  Cứ thế cho đến top-level
       ↓
  Nếu không ai bắt → Uncaught Error → Crash

Propagation Chain
  inner()
    ↓ throw
  middle()
    ↓ (không catch — dừng)
  outer()
    ↓ (không catch — dừng)
  Top-level → Crash

Quy tắc vàng:

  • throw là một statement, không phải expression. Không thể viết const x = throw ....
  • throw có thể ném bất kỳ value nào, nhưng convention là ném Error object.
  • Khi throw xảy ra, mọi code sau nó trong cùng block/function không chạy.
  • Nếu không có try/catch trên đường đi, error trở thành uncaught và script dừng (trong synchronous code).

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

Essential (Bắt buộc) ​

ConceptÝ nghĩa
throw statementthrow expression; — dừng execution hiện tại và truyền value lên call stack.
PropagationError đi ngược lại call stack cho đến khi được catch hoặc thoát ra ngoài.
Uncaught errorError không được xử lý. Trong browser: console error + script dừng. Trong Node.js: process exit với code ≠ 0.
Guard clause + throwif (!valid) throw new Error(...) — dừng sớm khi input không hợp lệ.
Throw Error objectthrow new TypeError("...") — mang đầy đủ message, stack, name.

Supporting (Hỗ trợ) ​

ConceptÝ nghĩa
Re-throwcatch một lỗi, xử lý một phần, rồi throw lại. Preview cho 0.7.3.
Throw in expression context workaroundthrow là statement nên không dùng trong expression trực tiếp. Dùng IIFE hoặc tách biệt.

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

ConceptLý do chưa đào sâu
Custom Error classclass ValidationError extends Error. Thuộc Stage 2 (Class/Object Model).
Error cause chainingnew Error("...", { cause: err }). ES2022. Thuộc Stage 9/13.

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

  • try/catch/finally mechanism. Thuộc 0.7.3.
  • Async throw / Promise rejection. Thuộc Stage 3.
  • Error Boundary trong React. Thuộc Stage 8.

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

Bài toán: Theo dõi propagation của error qua 3 tầng function.

js
function parseId(value) {
  if (typeof value !== "number") {
    throw new TypeError("ID must be a number");
  }
  if (value <= 0) {
    throw new RangeError("ID must be positive");
  }
  return value;
}

function fetchUser(rawId) {
  const id = parseId(rawId);
  return { id, name: "User " + id };
}

function renderUser(rawId) {
  const user = fetchUser(rawId);
  return `<h1>${user.name}</h1>`;
}

console.log(renderUser("abc"));

Output (V8):

text
TypeError: ID must be a number
    at parseId (app.js:3:11)
    at fetchUser (app.js:10:14)
    at renderUser (app.js:15:16)
    at Object.<anonymous> (app.js:18:13)

Walkthrough

Step 1 — Throw tại parseIdrawId là "abc", typeof là "string". Điều kiện !== "number" đúng → throw new TypeError(...). Dòng return value; không bao giờ chạy.

Step 2 — Propagation qua fetchUserfetchUser gọi parseId("abc"). Vì parseId throw, fetchUser cũng dừng ngay lập tức. Dòng return { id, name: ... } không chạy. Error object "bay qua" fetchUser.

Step 3 — Propagation qua renderUserrenderUser gọi fetchUser("abc"). fetchUser dừng do throw → renderUser cũng dừng. Dòng return `<h1>${user.name}</h1>` không chạy.

Step 4 — Top-levelrenderUser("abc") tại top-level throw. Không ai catch → uncaught error. Console in stack trace. Script dừng. console.log sau đó (nếu có) không chạy.

Step 5 — Đọc stack trace

  • Top frame: parseId — nơi throw.
  • Frame 2: fetchUser — gọi parseId.
  • Frame 3: renderUser — gọi fetchUser.
  • Frame 4: top-level — gọi renderUser.

Key Insight: throw là một cơ chế ngắt mạch. Nó không chỉ báo lỗi — nó ngăn chặn việc tiếp tục xử lý dữ liệu không hợp lệ qua nhiều tầng function.

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

Đừng chạy code. Đọc và dự đoán: (1) Có lỗi không? (2) Nếu có, message và stack trace sẽ như thế nào? (3) Dòng nào không chạy?

Câu 1 ​

js
function check(value) {
  if (!value) {
    throw new Error("Falsy value");
  }
  return value;
}
check(0);

Câu 2 ​

js
function a() { throw new Error("from a"); }
function b() { a(); return "b done"; }
function c() { b(); return "c done"; }
console.log(c());

Câu 3 ​

js
function validate(name) {
  if (typeof name !== "string") throw "Name must be string";
  if (name.length === 0) throw new Error("Name empty");
  return name.toUpperCase();
}
validate("");

Câu 4 ​

js
function step1() { return 1; }
function step2() { throw new Error("fail"); }
function step3() { return step1() + step2(); }
console.log(step3());

Câu 5 ​

js
function divide(a, b) {
  if (b === 0) throw new RangeError("Division by zero");
  return a / b;
}
function calculate() {
  const x = divide(10, 0);
  console.log("Result:", x);
  return x * 2;
}
calculate();

Câu 6 (Transfer — Throw vs Return) ​

js
function validate(x) {
  if (x < 0) throw new Error("negative");
  return x * 2;
}
function pipeline(x) {
  const a = validate(x);
  const b = a + 10;
  return b;
}
console.log(pipeline(5));
console.log(pipeline(-1));

Câu hỏi:

  1. console.log(pipeline(5)) in ra gì?
  2. Khi chạy pipeline(-1), những dòng nào trong pipeline không chạy? validate(-1) có return giá trị không?
  3. Nếu sửa throw new Error("negative") thành return null, pipeline(-1) sẽ ra kết quả gì? Điều này tốt hay xấu?
[Đáp án & Giải thích]

Câu 1: Error: Falsy value — 0 là falsy.

  • Giải thích: !0 là true nên throw. return value không chạy. Stack trace: check → top-level.

Câu 2: Error: from a — Propagation qua 3 tầng.

  • Giải thích: a() throw. b() dừng, "b done" không return. c() dừng, "c done" không return. console.log(c()) không chạy. Stack: a → b → c → top-level.

Câu 3: Error: Name empty — Throw primitive không xảy ra.

  • Giải thích: validate("") → typeof "" là "string" nên qua check đầu. name.length === 0 đúng → throw new Error("Name empty"). Lưu ý: nếu truyền 123, sẽ throw primitive "Name must be string" (string, không phải Error object).

Câu 4: Error: fail — step1() chạy xong rồi step2() throw.

  • Giải thích: step3() gọi step1() (trả về 1), rồi gọi step2() (throw). step3() dừng. 1 + step2() không evaluate kết quả. console.log không chạy.

Câu 5: RangeError: Division by zero — Dừng sớm, console.log không chạy.

  • Giải thích: divide(10, 0) throw. calculate() dừng tại const x = .... console.log("Result:", x) không chạy. return x * 2 không chạy. Stack: divide → calculate → top-level.

Câu 6:

  • Đáp án:

Câu 1: 20

  • Giải thích: validate(5) return 10. pipeline: a = 10, b = 20, return 20.

Câu 2: const b = a + 10 và return b không chạy. validate(-1) không return gì cả.

  • Giải thích: validate(-1) throw ngay tại if (x < 0). Dòng return x * 2 không chạy. pipeline dừng tại const a = validate(x). Các dòng sau đó trong pipeline không chạy. throw không tạo giá trị return — nó ngắt luồng hoàn toàn.

Câu 3: pipeline(-1) trả về 10 (vì null + 10 = 10). Điều này xấu.

  • Giải thích: return null che giấu lỗi. Caller nhận null nhưng pipeline vẫn chạy tiếp, tạo ra kết quả sai (10) thay vì báo lỗi. Đây chính là silent failure mà throw được thiết kế để ngăn chặn.

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

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

Viết function requireNumber throw TypeError nếu input không phải number.

js
function requireNumber(value) {
  if (___________) {
    throw new TypeError(___________);
  }
  return value;
}

// Test:
console.log(requireNumber(42)); // 42
// console.log(requireNumber("42")); // TypeError

Gợi ý

typeof value !== "number". Message: "Expected number, got " + typeof value.

[Đáp án tham khảo]
js
function requireNumber(value) {
  if (typeof value !== "number") {
    throw new TypeError("Expected number, got " + typeof value);
  }
  return value;
}

console.log(requireNumber(42)); // 42
// requireNumber("42");
// TypeError: Expected number, got string

Giải thích:

  • Guard clause kiểm tra kiểu sớm.
  • throw ngăn function tiếp tục với dữ liệu không hợp lệ.
  • Message rõ ràng giúp debug nhanh.

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

Hoàn thành function parseConfig để throw đúng loại lỗi.

js
function parseConfig(input) {
  if (input === null || typeof input !== "object") {
    throw new _______("Config must be an object");
  }
  if (!input.host) {
    throw new _______("Config must have 'host'");
  }
  if (typeof input.port !== "number") {
    throw new _______("Config.port must be a number");
  }
  if (input.port < 1 || input.port > 65535) {
    throw new _______("Config.port out of range");
  }
  return input;
}

// Test:
parseConfig({ host: "a.com", port: 80 }); // ✅
// parseConfig(null); // TypeError
// parseConfig({ port: 80 }); // Error
// parseConfig({ host: "a.com", port: "80" }); // TypeError
// parseConfig({ host: "a.com", port: 99999 }); // RangeError
[Đáp án tham khảo]
js
function parseConfig(input) {
  if (input === null || typeof input !== "object") {
    throw new TypeError("Config must be an object");
  }
  if (!input.host) {
    throw new Error("Config must have 'host'");
  }
  if (typeof input.port !== "number") {
    throw new TypeError("Config.port must be a number");
  }
  if (input.port < 1 || input.port > 65535) {
    throw new RangeError("Config.port out of range");
  }
  return input;
}

Giải thích:

  • Sai kiểu dữ liệu → TypeError.
  • Thiếu property bắt buộc → Error generic (hoặc TypeError tùy convention).
  • Giá trị ngoài phạm vi → RangeError.

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

Viết các function sau:

js
// 1. requireNonEmptyString(str) → Nếu không phải string, throw TypeError.
//    Nếu string rỗng, throw Error với message "String cannot be empty".

// 2. findById(items, id) → items là array. Nếu items không phải array, throw TypeError.
//    Nếu không tìm thấy item với id khớp, throw Error với message "Item not found".
//    Nếu tìm thấy, trả về item.

// 3. calculateArea(width, height) → Nếu width hoặc height <= 0, throw RangeError.
//    Nếu không phải number, throw TypeError. Trả về width * height.

// Sử dụng:
console.log(requireNonEmptyString("hello")); // "hello"
// console.log(requireNonEmptyString(""));    // Error
// console.log(requireNonEmptyString(123));   // TypeError

const items = [{ id: 1, name: "A" }, { id: 2, name: "B" }];
console.log(findById(items, 2)); // { id: 2, name: "B" }
// console.log(findById(items, 99)); // Error
// console.log(findById(null, 1));   // TypeError

console.log(calculateArea(5, 3)); // 15
// console.log(calculateArea(-1, 3)); // RangeError
// console.log(calculateArea("5", 3)); // TypeError
[Đáp án tham khảo]
js
function requireNonEmptyString(str) {
  if (typeof str !== "string") {
    throw new TypeError("Expected string");
  }
  if (str.length === 0) {
    throw new Error("String cannot be empty");
  }
  return str;
}

function findById(items, id) {
  if (!Array.isArray(items)) {
    throw new TypeError("Items must be an array");
  }
  const found = items.find(item => item.id === id);
  if (!found) {
    throw new Error("Item not found");
  }
  return found;
}

function calculateArea(width, height) {
  if (typeof width !== "number" || typeof height !== "number") {
    throw new TypeError("Width and height must be numbers");
  }
  if (width <= 0 || height <= 0) {
    throw new RangeError("Dimensions must be positive");
  }
  return width * height;
}

Giải thích:

  • requireNonEmptyString: Guard clause kiểm tra kiểu trước, rồi kiểm tra giá trị.
  • findById: Array.isArray là cách đáng tin cậy nhất để check array. find trả về undefined nếu không có — ta throw để báo lỗi rõ ràng thay vì trả về undefined gây nhầm lẫn.
  • calculateArea: Kiểm tra kiểu trước, phạm vi sau. Thứ tự quan trọng: nếu check <= 0 trước, "5" <= 0 vẫn đúng nhưng đó là coercion bug.

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

Throw là statement, không phải expression

js
const result = condition || throw new Error("fail");
// SyntaxError: Unexpected token 'throw'

Tại sao: throw là statement. Không thể dùng trong expression context như ||, ??, ternary.

Cách nhận biết: SyntaxError ngay khi parse.

Cách xử lý: Dùng if statement riêng biệt:

js
if (!condition) throw new Error("fail");
const result = condition;

Hoặc IIFE (không khuyến khích ở Stage 0):

js
const result = condition || (() => { throw new Error("fail"); })();

Lưu ý: Không thể dùng throw trong expression context (như ||, ??, ternary). Giải pháp duy nhất ở Stage 0 là tách thành statement riêng. Các pattern nâng cao như IIFE sẽ được học ở Stage 1.

Throw primitive mất stack trace

js
throw "Something went wrong";

Tại sao: String không có stack property. Khi log, bạn chỉ thấy message mà không biết lỗi từ đâu ra.

Cách nhận biết: Console chỉ in string, không có at function (file:line).

Cách xử lý: Luôn throw new Error("...").

Uncaught error dừng script

js
console.log("A");
throw new Error("fail");
console.log("B");

Tại sao: Không có try/catch. Error thoát ra top-level. Engine dừng execution.

Cách nhận biết: "B" không in ra. Console có stack trace đỏ.

Cách xử lý: Trong production, uncaught error là crash. Cần try/catch (0.7.3) hoặc global handler (Stage 9).

Throw trong return expression (không được)

js
function fn() {
  return throw new Error("x");
}

Tại sao: throw là statement, không phải expression. Không thể là toán hạng của return.

Cách nhận biết: SyntaxError.

Cách xử lý: Tách ra:

js
function fn() {
  throw new Error("x");
}

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

Symptom (Triệu chứng): Function trả về undefined thay vì throw khi input sai.

js
function getUser(id) {
  if (typeof id !== "number") {
    new Error("ID must be a number");
  }
  if (id <= 0) {
    throw "ID must be positive";
  }
  return { id };
}

const user = getUser("abc");
console.log(user.id);
// TypeError: Cannot read properties of undefined (reading 'id')

Reproduction (Tái hiện lỗi): Chạy code với getUser("abc").

Evidence (Bằng chứng):

  • user là undefined. Nghĩa là getUser đã chạy đến cuối mà không return gì (implicit undefined).
  • Không có error nào được throw cả.
  • Dòng new Error("...") tạo object nhưng không throw nó.

Hypothesis (Giả thuyết): Developer quên từ khóa throw. Họ viết new Error(...) nhưng không throw nó. Và ở check thứ hai, họ throw primitive string thay vì Error object.

Verification (Xác minh):

js
const err = new Error("test");
console.log(err); // Error object tồn tại, nhưng không dừng execution

Root Cause (Nguyên nhân gốc rễ):

  1. new Error(...) chỉ tạo object. throw mới kích hoạt propagation.
  2. throw "ID must be positive" là primitive, mất stack trace.

Fix (Sửa):

js
function getUser(id) {
  if (typeof id !== "number") {
    throw new TypeError("ID must be a number");
  }
  if (id <= 0) {
    throw new RangeError("ID must be positive");
  }
  return { id };
}

Prevention (Phòng ngừa):

  • Luôn kiểm tra: throw đi kèm new Error? Hay chỉ có new Error đơn độc?
  • Lint rule hoặc code review: "Naked new Error without throw".
  • Không throw primitive. Nếu thấy throw "...", sửa thành throw new Error("...").

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

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

js
function findUser(users, id) {
  const user = users.find(u => u.id === id);
  return user || null;
}
js
function findUser(users, id) {
  const user = users.find(u => u.id === id);
  if (!user) {
    throw new Error("User not found");
  }
  return user;
}

Câu hỏi:

  1. Option A có lợi gì khi "không tìm thấy" là kết quả bình thường (ví dụ: search)?
  2. Option B có lợi gì khi "không tìm thấy" là lỗi logic (ví dụ: lookup by ID từ database)?
  3. Nếu caller của Option A quên check null, bug gì xảy ra? Nếu caller của Option B không try/catch, bug gì xảy ra?
[Đáp án tham khảo]
  • Bạn nghĩ:

    1. Option A: Không làm gián đoạn luồng. Phù hợp khi "not found" là expected outcome. Caller tự quyết định xử lý.
    2. Option B: Bắt buộc caller đối mặt với lỗi. Không thể vô tình ignore. Phù hợp khi ID phải tồn tại theo business logic.
    3. Option A: Caller quên check → null.foo → TypeError ở xa nơi gốc. Bug âm thầm. Option B: Caller không catch → crash ngay tại findUser. Crash sớm, dễ debug hơn.
  • Kết luận: Throw khi "not found" là điều bất thường và không thể tiếp tục. Return null khi "not found" là một branch hợp lệ. Đừng dùng throw cho flow control bình thường.

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

Context: Bạn đang viết một utility function formatCurrency dùng trong nhiều component.

js
function formatCurrency(amount, currency) {
  return amount.toFixed(2) + " " + currency.toUpperCase();
}

// Một ngày nọ, API trả về:
formatCurrency(null, "usd");
// TypeError: Cannot read properties of null (reading 'toFixed')

Symptom: Trang checkout crash. Stack trace chỉ đến formatCurrency nhưng không rõ tại sao amount là null.

Constraint: Không thể sửa API ngay. Phải làm formatCurrency defensive.

Câu hỏi:

  1. Tại sao lỗi xảy ra ở formatCurrency thay vì nơi nhận data từ API?
  2. Viết lại formatCurrency để throw sớm với message rõ ràng nếu input không hợp lệ.
  3. Lợi ích của việc throw sớm (fail fast) trong trường hợp này là gì?
[Đáp án tham khảo]
  • Bạn nghĩ:

    1. formatCurrency giả định amount là number. Nó không validate input. Lỗi "nổ" ở utility function, xa nơi data bị corrupt.
    2. js
      function formatCurrency(amount, currency) {
        if (typeof amount !== "number") {
          throw new TypeError(`Expected number for amount, got ${typeof amount}`);
        }
        if (typeof currency !== "string") {
          throw new TypeError(`Expected string for currency, got ${typeof currency}`);
        }
        return amount.toFixed(2) + " " + currency.toUpperCase();
      }
    3. Throw sớm giúp stack trace chỉ đúng nơi data bị corrupt (hoặc nơi truyền sai). Không để lỗi lan truyền qua 5 tầng function rồi mới nổ ở một utility generic.
  • Bài học: "Fail fast" với throw + message rõ ràng giúp production debugging nhanh hơn gấp 10 lần so với để lỗi âm thầm lan truyền.

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

Level B — Challenge

  1. Tự trả lời trước: Viết đoạn code và dự đoán:
    js
    function check(x) {
      if (x < 0) throw "Negative";
      return x;
    }
    try {
      check(-1);
    } catch (e) {
      console.log(e.stack);
    }
  2. Hỏi AI: "Tại sao e.stack là undefined? Tôi đã throw một lỗi mà?"
  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 primitive string không có stack property và giải thích tại sao nên dùng new Error()?
  4. Verify bằng MDN: tìm "throw" trên MDN, đọc phần "Throwing a primitive".

Gợi ý

AI thường biết throw có thể ném primitive nhưng đôi khi giải thích mơ hồ bằng cách nói "throw can be any value". Nếu AI không chỉ ra rằng mất stack trace là hậu quả trực tiếp của throw primitive và điều này làm debug khó khăn trong production, bạn đã tìm ra điểm mù.

[Đáp án tham khảo]
  • Bạn nghĩ: e.stack là undefined vì e là string "Negative", không phải Error object. String không có property stack. Đây là lý do tại sao nên luôn throw new Error(...).

  • AI trả lời (typical):

    • throw can accept any expression, including primitives.
    • In your code, e is the string "Negative".
    • Strings do not have a stack property, so e.stack is undefined.
    • Best practice is to throw Error objects.
  • 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 quan trọng trong production: nếu bạn throw primitive và có global error handler (Sentry, Rollbar, v.v.), tool đó sẽ không có stack trace để group lỗi hoặc trace về nguồn.

  • Điểm AI nói sai hoặc quá mơ hồ: "Best practice is to throw Error objects" — đúng nhưng AI hiếm khi nói rõ: "Nếu codebase của bạn có convention throw primitive, bạn đang làm mất khả năng debug của toàn team."

  • Kết luận: Nếu bạn chỉ ra được rằng throw primitive = mất structured debugging information, bạn đã hiểu sâu hơn AI. Concept này sẽ quay lại ở Stage 3 (Promise rejection — luôn reject với Error object), Stage 9 (Error logging và monitoring), và Stage 13 (Incident response — stack trace là evidence).

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

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

"Em thấy người ta bảo không nên return null để báo lỗi mà nên throw. Nhưng throw làm app crash mà? Tại sao lại tốt hơn?"

Hãy giải thích trong 2 phút, dùng đúng terminology: propagation, fail fast, stack trace, silent failure.

Mô phỏng
  • Bạn nói: throw không tự làm app crash — không ai bắt mới làm app crash. Và điều đó tốt hơn return null rất nhiều.

Khi bạn return null để báo lỗi, caller nhận null và có thể nghĩ đó là kết quả hợp lệ. Rồi null đi qua 3 function nữa, cuối cùng null.name mới nổ TypeError. Lúc đó stack trace chỉ đến function cuối cùng — bạn mất dấu vết về nơi null xuất phát. Đây gọi là silent failure — lỗi âm thầm lan truyền.

Khi bạn throw, execution dừng ngay tại nơi phát hiện vấn đề. Error object chứa stack trace đầy đủ từ nơi throw qua các tầng function. Nếu bạn try/catch đúng chỗ (bài sau), bạn xử lý được. Nếu không, app crash ngay — nhưng crash sớm, crash gần nguồn, dễ debug hơn crash muộn ở xa.

💡 Tưởng tượng return null như một chiếc xe bị thủng lốp nhưng vẫn chạy — đến khi lật xe mới biết. throw như xe tự dừng ngay khi phát hiện lốp thủng — bạn biết ngay vấn đề ở đâu.

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

  • Đồng nghiệp có hiểu tại sao crash sớm tốt hơn silent failure không?
  • Bạn có tránh được việc nói "throw tốt hơn vì người ta bảo thế" không?
  • Nếu đồng nghiệp hỏi: "Vậy khi nào em KHÔNG nên throw?" — bạn trả lời được không? (Gợi ý: khi "not found" là kết quả hợp lệ, hoặc khi bạn đang viết một hàm validation nhẹ và muốn trả về boolean.)
  • Nếu đồng nghiệp viết throw "error" — bạn giải thích được tại sao nên dùng new Error không?

15. Assessment (Đánh giá) ​

Objective (Mục tiêu)Hình thức đánh giáTask
Throw dừng executionPrediction (Dự đoán)Prediction Câu 2, 4, 5
Throw Error objectImplementation (Thực hành)Implementation Lab Level 1–3
Phân biệt throw primitive vs ErrorAI-assistedAI-assisted Section
Propagation qua call stackPrediction (Dự đoán)Prediction Câu 2
Guard clause + throwImplementation (Thực hành)Implementation Lab Level 2–3
Fail fast vs return nullDesign ExerciseDesign Exercise Section
Explain propagationTeach BackTeach Back Section

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

  • [ ] Có thể giải thích throw dừng execution và truyền value lên call stack.
  • [ ] Có thể sử dụng throw với Error object để báo lỗi rõ ràng.
  • [ ] Có thể dự đoán luồng execution khi throw xuất hiện trong nested calls (≥ 4/5 scenarios đúng).
  • [ ] Có thể phân biệt throw Error object và throw primitive, và giải thích tại sao primitive là anti-pattern.
  • [ ] Có thể viết guard clause kết hợp throw để validate input sớm.
  • [ ] Có thể debug lỗi do quên từ khóa throw trước new Error.
  • [ ] Có thể giải thích trade-off giữa throw và return null trong design scenario.

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

Previous (Trước): Errors (0.7.1) — bạn đã biết error object chứa message, name, stack. Giờ bạn học cách kích hoạt error object để nó di chuyển trong chương trình.

Current (Hiện tại): Throw — throw statement, propagation lên call stack, uncaught error, guard clause + throw, fail fast.

Next (Tiếp theo):

  • 0.7.3 (Try/Catch) — Bắt error đang propagate. try/catch là "lưới" để bắt bóng lỗi trước khi nó thoát ra ngoài. Nếu không có try/catch, throw = crash.
  • 0.7.4 (Defensive Programming) — Kết hợp guard clause, throw, và validation để viết code chống chịu lỗi.
  • Stage 1 (Execution Model) — Call stack. Propagation của throw chính là unwinding của call stack. Hiểu execution context giúp trace throw chính xác.
  • Stage 3 (Async) — throw trong async function trở thành Promise rejection. Propagation khác synchronous.
  • Stage 8 (React) — Error Boundaries catch throw từ component tree. Throw trong render phase.
  • Stage 9/13 (Production) — Global error handlers, logging services (Sentry), và incident response. Stack trace từ throw là evidence chính.
📴 Offline Mode — Content served from cache