Skip to content

Lesson 0.7.3 — Try/Catch ​

Bài 0.7.3 — Try/Catch
try, catch, finally & Xử lý lỗi đồng bộ • 35 phút
0:00 / 0:00

0. Metadata ​

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

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

Bạn đã biết throw ném error lên call stack. Nếu không ai bắt, script crash. Nhưng trong production, "crash" không phải lúc nào cũng là lựa chọn tốt — đôi khi bạn cần bắt lỗi, ghi log, và tiếp tục hoặc bắt lỗi, dọn dẹp resource, và báo lỗi graceful.

Xét đoạn code:

js
function readConfig(path) {
  const data = JSON.parse(readFile(path));
  return data;
}

readConfig("app.json");
// Nếu file không tồn tại hoặc JSON sai → crash toàn bộ app

Vấn đề cốt lõi

throw là cơ chế propagation. Nhưng propagation không có điểm dừng thì thành crash. try/catch là điểm dừng có kiểm soát trên đường đi của error.

try/catch cho phép bạn:

  • Bắt error tại một điểm cụ thể.
  • Phân loại error để xử lý khác nhau.
  • Dọn dẹp resource (file, network, timer) dù thành công hay thất bại.
  • Rethrow nếu lỗi này không thuộc trách nhiệm của bạn.

Nhưng try/catch cũng là nguồn của một anti-pattern nguy hiểm: catch-all + silent failure. Bài này dạy bạn dùng try/catch như một cổng kiểm soát, không phải tấm chắn vô điều kiện.

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 name, message, stack (0.7.1).
  • Biết throw dừng execution và propagate lên call stack (0.7.2).
  • Hiểu if/else và block scope (0.4.2).
  • Biết typeof và instanceof (0.2.5, preview Stage 2).

WARNING

Nếu bạn chưa chắc tại sao throw làm script crash khi không có try/catch, quay lại 0.7.2. try/catch chỉ có ý nghĩa khi bạn hiểu đường đi của error trên call stack.

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

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

  1. Sử dụng try/catch để bắt error và ngăn script crash.
  2. Sử dụng finally để đảm bảo cleanup code chạy dù thành công hay lỗi.
  3. Truy cập error object trong catch và đọc name, message, stack.
  4. Phân loại error bằng instanceof hoặc name để xử lý có chọn lọc.
  5. Thực hiện rethrow khi error không thuộc phạm vi xử lý hiện tại.
  6. Nhận diện anti-pattern catch-all và silent failure.
  7. Dự đoán luồng execution khi có try/catch/finally và throw.

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

Mental Model

Try/Catch = Cổng kiểm soát trên đường cao tốc của error

  try {
    // Đường cao tốc bình thường
    riskyOperation();
    successCode();
  }
       ↓ throw xảy ra
  catch (error) {
    // Cổng chặn — bắt error đang bay lên
    handle(error);
  }
       ↓ (luôn chạy)
  finally {
    // Trạm dọn dẹp — chạy dù thành công hay lỗi
    cleanup();
  }

Propagation với Try/Catch
  inner() throw
    ↓
  middle() không catch → error bay qua
    ↓
  outer() có catch → error dừng lại ở đây
    ↓
  Nếu outer() rethrow → error tiếp tục bay lên

Quy tắc vàng:

  • try bắt đầu một "vùng được bảo vệ". Nếu bất kỳ dòng nào trong try throw, execution nhảy ngay đến catch.
  • catch chỉ chạy khi có error. Nếu try thành công, catch bị bỏ qua.
  • finally chạy luôn luôn — sau try thành công, sau catch, hoặc sau try có return/throw (trừ khi process bị kill).
  • catch (e) nhận error object. Bạn có thể inspect e.name, e.message, e.stack.
  • Nếu catch không xử lý xong và không rethrow, error coi như "đã giải quyết" — không propagate tiếp.

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

Essential (Bắt buộc) ​

ConceptÝ nghĩa
try blockVùng code có thể throw. Nếu throw, nhảy đến catch.
catch (error)Bắt error từ try. error là parameter chứa thrown value.
finally blockChạy luôn luôn sau try hoặc catch. Dùng cho cleanup.
Rethrowcatch xử lý một phần, rồi throw error; để propagate tiếp.
Phân loại lỗiif (e instanceof TypeError) hoặc e.name === "TypeError" để xử lý có chọn lọc.
Catch-all anti-patterncatch (e) { console.log(e); } — bắt mọi lỗi nhưng không xử lý thực sự.

Supporting (Hỗ trợ) ​

ConceptÝ nghĩa
try...finally (không catch)Chỉ dùng finally để cleanup mà không bắt lỗi. Lỗi vẫn propagate.
Error trong catch/finallyNếu catch hoặc finally throw error mới, error mới đè lên error cũ.

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

ConceptLý do chưa đào sâu
Conditional catch (optional binding)catch { ... } (ES2019) — không cần parameter nếu không dùng error.
Error.causeChain lỗi khi rethrow. ES2022. Stage 9/13.

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

  • Async try/catch với await (Promise rejection). Thuộc Stage 3.
  • try/catch performance cost trong hot path. Thuộc Stage 11.
  • Domain-specific error recovery strategies (retry, circuit breaker). Thuộc Stage 5/9.

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

Bài toán: Đọc và parse JSON từ input, xử lý các loại lỗi khác nhau.

js
function parseConfig(input) {
  try {
    const parsed = JSON.parse(input);
    if (typeof parsed !== "object" || parsed === null) {
      throw new TypeError("Config must be an object");
    }
    return parsed;
  } catch (error) {
    // instanceof kiểm tra error có phải instance của SyntaxError không
    // (Cách khác không cần instanceof: error.name === "SyntaxError")
    if (error instanceof SyntaxError) {
      console.error("Invalid JSON format:", error.message);
      return { default: true };
    }
    if (error instanceof TypeError) {
      console.error("Wrong config type:", error.message);
      return { default: true };
    }
    throw error; // Rethrow — không phải lỗi ta biết cách xử lý
  } finally {
    console.log("Parse attempt completed");
  }
}

console.log(parseConfig('{"port": 3000}'));
console.log(parseConfig("not json"));

Output:

text
Parse attempt completed
{ port: 3000 }
Invalid JSON format: Unexpected token 'o', "not json" is not valid JSON
Parse attempt completed
{ default: true }

Walkthrough

Step 1 — try thành côngJSON.parse('{"port": 3000}') thành công. parsed là object. if sai. return parsed. catch bị bỏ qua. finally chạy.

Step 2 — try throw SyntaxErrorJSON.parse("not json") throw SyntaxError. Execution trong try dừng ngay lập tức. Nhảy đến catch (error).

Step 3 — Phân loại trong catcherror instanceof SyntaxError là true. Log message. Return fallback { default: true }. finally vẫn chạy sau catch.

Step 4 — Rethrow Nếu JSON.parse thành công nhưng parsed là number (ví dụ "123"), dòng throw new TypeError(...) trong try ném TypeError. catch bắt được, instanceof TypeError đúng → log và return fallback.

Nếu một lỗi khác xảy ra (ví dụ: RangeError từ một hàm khác), catch không match instanceof nào → throw error; rethrow. Error tiếp tục propagate lên.

Step 5 — finally luôn chạy Dù try thành công, catch chạy, hay catch rethrow — finally đều in "Parse attempt completed".

💡 Key Insight: catch không phải là "bắt mọi thứ rồi nuốt". catch là "bắt, phân loại, xử lý những gì mình hiểu, và trả lại những gì mình không hiểu". Rethrow là cách bảo vệ bạn khỏi việc âm thầm swallow lỗi nghiêm trọng.

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

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

  • (1) Output là gì?
  • (2) finally chạy mấy lần?
  • (3) Có error nào thoát ra ngoài không?

Câu 1 ​

js
function test() {
  try {
    return "try";
  } finally {
    console.log("finally");
  }
}
console.log(test());

Câu 2 ​

js
function test() {
  try {
    throw new Error("A");
  } catch (e) {
    console.log("catch:", e.message);
  } finally {
    console.log("finally");
  }
}
test();

Câu 3 ​

js
function test() {
  try {
    throw new Error("A");
  } catch (e) {
    throw new Error("B");
  } finally {
    console.log("finally");
  }
}
try {
  test();
} catch (e) {
  console.log("outer:", e.message);
}

Câu 4 ​

js
let count = 0;
function risky() {
  count++;
  if (count === 1) throw new TypeError("fail");
  return "ok";
}
try {
  console.log(risky());
} catch (e) {
  console.log("retry");
  console.log(risky());
}

Câu 5 ​

js
try {
  console.log("A");
} catch (e) {
  console.log("B");
} finally {
  console.log("C");
}

Câu 6 (Transfer — Phân loại lỗi) ​

js
function process(data) {
  try {
    const parsed = JSON.parse(data);
    return parsed.value.toUpperCase();
  } catch (e) {
    if (e instanceof SyntaxError) return "BAD_JSON";
    return "UNKNOWN";
  }
}

console.log(process('{"value": "hello"}'));
console.log(process("not json"));
console.log(process('{"value": 123}'));

Câu hỏi:

  1. Dự đoán output của 3 lần gọi.
  2. Lần gọi thứ 3 trả về "UNKNOWN" — điều này có phải là xử lý đúng không? Tại sao? Nếu không, nên sửa như thế nào?
[Đáp án & Giải thích]

Câu 1:

text
finally
try
  • Giải thích: finally chạy trước return thực sự thoát khỏi function. return "try" được "tạm giữ", finally chạy xong mới return. Đây là behavior đặc biệt của finally.

Câu 2:

text
catch: A
finally
  • Giải thích: try throw → catch bắt được, log "catch: A". finally chạy sau catch. Không có error thoát ra ngoài.

Câu 3:

text
finally
outer: B
  • Giải thích: try throw "A" → catch bắt, nhưng catch lại throw "B". finally vẫn chạy trước khi error "B" thoát ra. Outer catch bắt "B". Error "A" bị thay thế (replaced) bởi "B" — JavaScript engine chỉ giữ completion value mới nhất, stack trace của "A" bị discard. Đây là điểm mù nguy hiểm của throw trong catch.

Câu 4:

text
retry
ok
  • Giải thích: Lần 1 risky() throw TypeError. catch log "retry". Lần 2 risky() count === 2, return "ok". catch không bắt lần 2 vì lần 2 không throw.

Câu 5:

text
A
C
  • Giải thích: try thành công, không có lỗi. catch bị bỏ qua. finally chạy.

Câu 6:

text
HELLO
BAD_JSON
UNKNOWN
  • Giải thích:

    1. JSON.parse thành công, parsed.value là "hello", .toUpperCase() → "HELLO".
    2. JSON.parse throw SyntaxError → catch trả về "BAD_JSON".
    3. JSON.parse thành công ({"value": 123}), nhưng parsed.value là 123 (number). 123.toUpperCase() throw TypeError. catch không check instanceof TypeError nên rơi vào return "UNKNOWN".
  • Vấn đề lần gọi thứ 3: Trả về "UNKNOWN" che giấu một TypeError thực sự — đây là silent failure. Nên sửa:

    js
    catch (e) {
      if (e instanceof SyntaxError) return "BAD_JSON";
      throw e; // Rethrow TypeError để caller biết
    }

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 loadData(source) {
  let connection;
  try {
    connection = openConnection(source);
    const data = connection.read();
    return data;
  } catch (e) {
    console.error("Load failed:", e.message);
    return null;
  } finally {
    if (connection) connection.close();
  }
}
  1. Nếu openConnection throw, connection.close() trong finally có chạy không? Tại sao cần check if (connection)?
  2. Nếu connection.read() throw, connection.close() có chạy không?
  3. Nếu mọi thứ thành công, connection.close() có chạy không?
[Đáp án & Giải thích]
  1. Có chạy, nhưng connection là undefined (vì openConnection throw trước khi gán). Nếu không check if (connection), undefined.close() sẽ gây TypeError mới trong finally — đè lên error gốc. Đây là bug production phổ biến.
  2. Có chạy. read() throw → catch chạy → finally chạy. connection đã được gán nên close() an toàn.
  3. Có chạy. try thành công, return data được "tạm giữ", finally chạy, rồi mới thực sự return.

Bài học: finally là nơi cleanup, nhưng phải defensive vì bạn không biết try đã chạy đến đâu trước khi lỗi.

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

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

Viết function safeParse dùng try/catch để parse JSON. Nếu lỗi, trả về null. Luôn log "Parsing done" dù thành công hay thất bại.

js
function safeParse(json) {
  try {
    return _______;
  } catch (_______) {
    return _______;
  } finally {
    _______;
  }
}

console.log(safeParse('{"a": 1}')); // { a: 1 }
console.log(safeParse("bad"));      // null

Gợi ý

return JSON.parse(json);, catch (e), return null;, console.log("Parsing done").

[Đáp án tham khảo]
js
function safeParse(json) {
  try {
    return JSON.parse(json);
  } catch (e) {
    return null;
  } finally {
    console.log("Parsing done");
  }
}

console.log(safeParse('{"a": 1}')); // { a: 1 }
// Parsing done
console.log(safeParse("bad"));      // null
// Parsing done

Giải thích:

  • try parse JSON. Thành công → return kết quả.
  • Lỗi → catch bắt, return null.
  • finally chạy trong cả hai trường hợp.

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

Hoàn thành function calculate để bắt TypeError và RangeError riêng biệt, rethrow các lỗi khác.

js
function calculate(a, b, op) {
  try {
    if (typeof a !== "number" || typeof b !== "number") {
      throw new TypeError("Operands must be numbers");
    }
    if (op === "/" && b === 0) {
      throw new RangeError("Division by zero");
    }
    if (op === "+") return a + b;
    if (op === "-") return a - b;
    if (op === "*") return a * b;
    if (op === "/") return a / b;
    throw new Error("Unknown operator");
  } catch (_______) {
    if (_______ instanceof TypeError) {
      return "TYPE_ERROR";
    }
    if (_______ instanceof RangeError) {
      return "RANGE_ERROR";
    }
    _______; // Rethrow lỗi khác
  }
}

// Test:
console.log(calculate(10, 2, "+")); // 12
console.log(calculate("10", 2, "+")); // TYPE_ERROR
console.log(calculate(10, 0, "/"));   // RANGE_ERROR
// console.log(calculate(10, 2, "%")); // Error: Unknown operator
[Đáp án tham khảo]
js
function calculate(a, b, op) {
  try {
    if (typeof a !== "number" || typeof b !== "number") {
      throw new TypeError("Operands must be numbers");
    }
    if (op === "/" && b === 0) {
      throw new RangeError("Division by zero");
    }
    if (op === "+") return a + b;
    if (op === "-") return a - b;
    if (op === "*") return a * b;
    if (op === "/") return a / b;
    throw new Error("Unknown operator");
  } catch (e) {
    if (e instanceof TypeError) {
      return "TYPE_ERROR";
    }
    if (e instanceof RangeError) {
      return "RANGE_ERROR";
    }
    throw e; // Rethrow lỗi khác
  }
}

Giải thích:

  • catch (e) bắt mọi lỗi từ try.
  • instanceof phân loại để xử lý có chọn lọc.
  • throw e; ở cuối catch đảm bảo lỗi không thuộc hai loại trên vẫn propagate ra ngoài. Không bị silent failure.

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

Viết các function sau:

js
// 1. safeRead(obj, key) → Trả về obj[key]. Nếu obj là null/undefined,
//    không throw mà trả về undefined. Dùng try/catch.

// 2. withCleanup(task, cleanup) → Chạy task() trong try. Dù task() throw hay return,
//    luôn gọi cleanup() trong finally. Trả về kết quả của task() hoặc throw nếu task() throw.

// 3. retryOnce(fn) → Gọi fn() trong try. Nếu throw, gọi lại fn() một lần nữa.
//    Nếu lần 2 vẫn throw, để lỗi thoát ra. Không dùng vòng lặp.

// Sử dụng:
const data = { name: "Alice" };
console.log(safeRead(data, "name")); // "Alice"
console.log(safeRead(null, "name")); // undefined

let cleaned = false;
const result = withCleanup(
  () => "done",
  () => { cleaned = true; }
);
console.log(result, cleaned); // "done" true

let calls = 0;
const flaky = () => {
  calls++;
  if (calls === 1) throw new Error("fail");
  return "ok";
};
console.log(retryOnce(flaky)); // "ok"
console.log(calls); // 2
[Đáp án tham khảo]
js
function safeRead(obj, key) {
  try {
    return obj[key];
  } catch (e) {
    return undefined;
  }
}

function withCleanup(task, cleanup) {
  try {
    return task();
  } finally {
    cleanup();
  }
}

function retryOnce(fn) {
  try {
    return fn();
  } catch (e) {
    return fn(); // Lần 2, nếu throw sẽ thoát ra ngoài
  }
}

Giải thích:

  • safeRead: null[key] throw TypeError. catch trả về undefined — defensive pattern khi access external data.
  • ⚠️ Lưu ý: Đây là catch-all đơn giản. Nếu obj là object hợp lệ nhưng getter trên key throw lỗi, function cũng trả về undefined. Ở production, nên phân loại e.name cụ thể hơn thay vì bắt tất cả.
  • withCleanup: finally đảm bảo cleanup() chạy dù task() return hay throw. Không cần catch nếu chỉ muốn cleanup và propagate lỗi.
  • retryOnce: Lần 1 throw → catch chạy, gọi lại fn(). Lần 2 thành công → return. Lần 2 throw → không có catch nào khác → propagate.

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

Catch-all + silent failure

js
try {
  riskyOperation();
} catch (e) {
  // Không làm gì cả
}

Tại sao: Bắt mọi lỗi nhưng không log, không xử lý, không rethrow. Bug nghiêm trọng bị chôn vùi.

Cách nhận biết: Code chạy "bình thường" nhưng kết quả sai. Không có trace để debug.

Cách xử lý: Ít nhất phải log. Tốt hơn là phân loại và chỉ catch những lỗi bạn hiểu.

js
catch (e) {
  if (e instanceof KnownError) {
    handle(e);
  } else {
    throw e;
  }
}

Throw trong catch ghi đè error gốc

js
try {
  throw new Error("Original");
} catch (e) {
  throw new Error("New");
}

Tại sao: Error "Original" bị mất hoàn toàn. Stack trace chỉ còn "New".

Cách nhận biết: Stack trace không chỉ đến nguồn lỗi thực sự.

Cách xử lý: Nếu cần wrap error, giữ lại thông tin gốc (ES2022 cause, hoặc log trước khi throw):

js
catch (e) {
  console.error("Original:", e);
  throw new Error("New: " + e.message);
}

finally chạy trước return

js
function test() {
  try {
    return "try";
  } finally {
    return "finally";
  }
}
console.log(test()); // "finally"

Tại sao: finally có thể override return từ try hoặc catch. Đây là behavior ít người biết.

Cách nhận biết: Return value bất ngờ.

Cách xử lý: Không return trong finally.

catch parameter scope

js
try {
  throw new Error("x");
} catch (e) {
  console.log(e.message);
}
console.log(e); // ReferenceError: e is not defined

Tại sao: e chỉ tồn tại trong block catch.

Cách nhận biết: ReferenceError khi access e ngoài catch.

Cách xử lý: Nếu cần error bên ngoài, gán vào biến outer scope trong catch.

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

Symptom (Triệu chứng): App không crash nhưng data bị mất không lý do.

js
function saveUser(user) {
  try {
    validate(user);
    db.insert(user);
    return true;
  } catch (e) {
    console.log("Error saved");
    return false;
  }
}

const result = saveUser(null);
console.log(result); // false
// Không biết tại sao fail

Reproduction (Tái hiện lỗi): Chạy saveUser(null).

Evidence (Bằng chứng):

  • Console chỉ in "Error saved". Không có stack trace. Không biết lỗi gì.
  • validate(null) có thể throw TypeError hoặc ReferenceError. Hoặc db.insert(null) throw. Hoàn toàn không rõ.
  • catch bắt mọi thứ và chỉ log một dòng generic.

Hypothesis (Giả thuyết): Developer sợ app crash nên bọc try/catch quá rộng. Họ nghĩ "bắt hết là an toàn". Nhưng điều này tạo ra silent failure — lỗi bị nuốt mà không có thông tin để debug.

Verification (Xác minh):

js
// Thêm log tạm để xem error thực sự:
catch (e) {
  console.log("ACTUAL ERROR:", e.name, e.message, e.stack);
}

Root Cause (Nguyên nhân gốc rễ): Catch-all không phân loại. Không rethrow lỗi không mong đợi. console.log("Error saved") không chứa bất kỳ thông tin debug nào.

Fix (Sửa):

js
function saveUser(user) {
  try {
    validate(user);
    db.insert(user);
    return true;
  } catch (e) {
    if (e instanceof ValidationError) {
      console.error("Validation failed:", e.message);
      return false;
    }
    throw e; // Lỗi DB hoặc lỗi không mong đợi → propagate
  }
}

Hoặc ở hiện tại Stage 0 (chưa có custom class):

js
function saveUser(user) {
  try {
    validate(user);
    db.insert(user);
    return true;
  } catch (e) {
    if (e.name === "ValidationError") {
      console.error("Validation failed:", e.message);
      return false;
    }
    console.error("Unexpected error during save:", e);
    throw e;
  }
}

Ghi chú: instanceof ValidationError chỉ hoạt động nếu bạn định nghĩa custom class ValidationError (Stage 2). Ở Stage 0, dùng e.name để phân loại.

Prevention (Phòng ngừa):

  • Catch chỉ những lỗi bạn biết cách xử lý.
  • Nếu catch generic, phải log đầy đủ name, message, stack.
  • Luôn để lối thoát cho lỗi không mong đợi: throw e ở cuối catch.
  • Hỏi: "Nếu lỗi này xảy ra lúc 3 giờ sáng, tôi có đủ thông tin để debug không?"

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

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

js
function fetchData(url) {
  try {
    return httpGet(url);
  } catch (e) {
    return { error: true, data: null };
  }
}
js
function fetchData(url) {
  try {
    return httpGet(url);
  } catch (e) {
    if (e.name === "NetworkError") {
      return { error: true, data: null };
    }
    throw e;
  }
}

Câu hỏi:

  1. Option A có lợi gì về đơn giản? Rủi ro gì trong production?
  2. Option B có lợi gì khi debug incident?
  3. Nếu httpGet throw TypeError do bug code (ví dụ: gọi .trim() trên undefined), option nào giúp phát hiện sớm hơn?
[Đáp án tham khảo]
  • Bạn nghĩ:

    1. Option A: Đơn giản, caller luôn nhận về object có shape cố định. Rủi ro: Bug nghiêm trọng (TypeError, ReferenceError) bị nuốt. App trả về { error: true } thay vì crash — bạn không biết có bug cho đến khi user báo.
    2. Option B: Chỉ swallow lỗi network (expected). Các lỗi code (unexpected) vẫn propagate và được monitoring tool bắt. Dễ phân biệt "lỗi mạng" và "lỗi code".
    3. Option B. TypeError sẽ thoát ra ngoài, được log với stack trace đầy đủ. Option A sẽ trả về { error: true } và bạn mất dấu vết bug.
  • Kết luận: Catch chỉ những gì bạn dự đoán được và có kế hoạch xử lý. Mọi thứ khác nên được biết đến (crash + log) thay vì bị chôn vùi.

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

Context: Bạn đang viết một batch processor xử lý 1000 records.

js
function processRecords(records) {
  const results = [];
  for (const record of records) {
    try {
      const result = transform(record);
      results.push(result);
    } catch (e) {
      // Skip bad records
    }
  }
  return results;
}

Symptom: Sau khi chạy, chỉ có 800 kết quả. 200 records bị "skip" nhưng không biết tại sao. Không có log.

Constraint: Không thể dừng toàn bộ batch vì một vài records lỗi. Nhưng cần biết records nào lỗi để investigate.

Câu hỏi:

  1. Tại sao empty catch là nguy hiểm trong production?
  2. Viết lại để vẫn skip bad records nhưng log đủ thông tin (record index, error message).
  3. Nếu một record gây lỗi không phải do data mà do bug code (ví dụ: transform có ReferenceError), bạn có muốn skip không?
[Đáp án tham khảo]
  • Bạn nghĩ:

    1. Empty catch = silent failure. Bạn mất 20% data mà không biết lý do. Không thể reproduce.
    2. js
      function processRecords(records) {
        const results = [];
        for (let i = 0; i < records.length; i++) {
          try {
            const result = transform(records[i]);
            results.push(result);
          } catch (e) {
            console.error(`Record ${i} failed:`, e.message, e.stack);
            // Hoặc push vào dead-letter queue
          }
        }
        return results;
      }
    3. Không. ReferenceError là bug code, không phải bad data. Nên rethrow hoặc dừng batch để fix code. Chỉ nên catch và skip lỗi domain-related (ví dụ: ValidationError).
  • Bài học: Trong production, "không crash" không đồng nghĩa "hoạt động đúng". Empty catch biến "hoạt động sai" thành "hoạt động im lặng".

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:
    js
    function test() {
      try {
        return "A";
      } catch (e) {
        return "B";
      } finally {
        console.log("C");
      }
    }
    console.log(test());
  2. Hỏi AI: "Tại sao finally chạy trước khi return 'A' thực sự thoát function? Điều này có nghĩa là finally có thể thay đổi return value không?"
  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 finally chạy trước return thực sự và có thể override return value?
  4. Verify bằng MDN: tìm "try...catch" trên MDN, đọc phần "The finally block".

Gợi ý

AI thường biết finally "luôn chạy" nhưng đôi khi giải thích mơ hồ bằng cách nói "finally executes after try and catch". Nếu AI không chỉ ra rằng finally chạy trước khi control trả về caller (và có thể override return/throw), bạn đã tìm ra điểm mù. AI cũng có thể bỏ qua việc finally có return sẽ override try return.

[Đáp án tham khảo]
  • Bạn nghĩ: Output là "C" rồi "A". finally chạy trước khi return "A" thực sự thoát function. Nếu finally có return "D", kết quả sẽ là "D" — finally có thể override.

  • AI trả lời (typical):

    • The finally block executes after the try and catch blocks.
    • It runs regardless of whether an exception was thrown.
    • If finally contains a return, it can override the return value from try.
  • 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 viết cleanup trong finally và vô tình return một giá trị từ đó, bạn sẽ gây bug khó trace.

  • Điểm AI nói sai hoặc quá mơ hồ: "finally executes after try and catch" — đúng nhưng dễ hiểu nhầm là "sau khi function đã return". AI hiếm khi nói rõ: "finally chạy trong cùng một execution context, trước khi control truyền về caller."

  • Kết luận: Nếu bạn chỉ ra được rằng finally là một completion record interceptor (trong spec terms) và có thể thay đổi kết quả của try/catch, bạn đã hiểu sâu hơn AI. Concept này sẽ quay lại ở Stage 1 (Execution Model — completion records), Stage 3 (Async cleanup với finally trong Promise), và Stage 9 (Resource management pattern).

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 bọc code trong try/catch để app không crash. Vậy em bọc toàn bộ app trong một try/catch lớn có được không?"

Hãy giải thích trong 2 phút, dùng đúng terminology: catch-all, silent failure, rethrow, phân loại lỗi.

Mô phỏng
  • Bạn nói: Không được. Bọc toàn bộ app trong một try/catch lớn giống như lắp một cái máy hút bụi khổng lồ trên trần nhà — mọi thứ rơi xuống đều biến mất mà bạn không biết là cái gì.

try/catch không phải để "ngăn crash bằng mọi giá". Nó là để bắt lỗi bạn hiểu và có kế hoạch xử lý. Ví dụ: bạn biết API có thể timeout → catch NetworkError và retry. Nhưng nếu bạn catch cả TypeError do bug code, bạn đang nuốt lỗi (silent failure).

Cách đúng:

  • Catch những lỗi expected và recoverable.
  • Log đầy đủ name, message, stack.
  • Nếu không hiểu lỗi → rethrow để monitoring tool bắt và báo động.

💡 Tưởng tượng try/catch như bộ lọc nước. Bạn không lắp bộ lọc để biến nước bẩn thành nước sạch mà không biết bẩn ở đâu. Bạn lắc bộ lọc để tách cặn bã (lỗi expected) và báo động nếu phát hiện chì (lỗi nghiêm trọng).

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

  • Đồng nghiệp có hiểu tại sao catch-all là anti-pattern không?
  • Bạn có tránh được việc nói "try/catch tốt hơn không có" một cách tuyệt đối không?
  • Nếu đồng nghiệp hỏi: "Vậy em nên catch ở đâu?" — bạn trả lời được không? (Gợi ý: ở boundary — API call, file I/O, user input — nơi bạn có thông tin để phân loại.)
  • Nếu đồng nghiệp viết catch (e) { console.log(e) } — bạn giải thích được tại sao nên log e.stack và rethrow nếu không xử lý không?

15. Assessment (Đánh giá) ​

Objective (Mục tiêu)Hình thức đánh giáTask
Try/catch syntaxImplementation (Thực hành)Implementation Lab Level 1
Finally behaviorPrediction (Dự đoán)Prediction Câu 1, 2, 3
Error object trong catchImplementation (Thực hành)Implementation Lab Level 2
Phân loại lỗi (instanceof)Implementation (Thực hành)Implementation Lab Level 2
RethrowImplementation (Thực hành)Implementation Lab Level 2–3
Catch-all anti-patternDebug Lab / Design ExerciseDebug Lab & Design Exercise
Silent failureDebug LabDebug Lab Section
Explain finally overrideAI-assisted / PredictionAI-assisted Section, Prediction Câu 1
Explain catch-all anti-patternTeach BackTeach Back Section

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

  • [ ] Có thể sử dụng try/catch để bắt error và ngăn script crash.
  • [ ] Có thể sử dụng finally để đảm bảo cleanup code chạy dù thành công hay lỗi.
  • [ ] Có thể truy cập và đọc name, message, stack từ error object trong catch.
  • [ ] Có thể phân loại error bằng instanceof hoặc name để xử lý có chọn lọc.
  • [ ] Có thể thực hiện rethrow khi error không thuộc phạm vi xử lý hiện tại.
  • [ ] Có thể nhận diện và tránh catch-all anti-pattern.
  • [ ] Có thể debug lỗi do empty catch hoặc catch quá rộng gây silent failure.
  • [ ] Có thể dự đoán luồng execution khi có try/catch/finally và throw (≥ 4/5 scenarios đúng).

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

Previous (Trước): Throw (0.7.2) — bạn đã biết throw ném error lên call stack. Giờ bạn học cách dừng error đang bay bằng try/catch và dọn dẹp bằng finally.

Current (Hiện tại): Try/Catch — bắt error, phân loại, rethrow, cleanup với finally, anti-pattern catch-all.

Next (Tiếp theo):

  • 0.7.4 (Defensive Programming) — Kết hợp guard clause, validation, và try/catch có chọn lọc để viết code chống chịu lỗi.
  • 0.7.5 (Readable JavaScript) — Error handling là một phần của readability. Code xử lý lỗi rõ ràng dễ maintain hơn code "bọc try/catch khắp nơi".
  • Stage 1 (Execution Model) — Call stack unwinding khi throw. try/catch tạo một "catching boundary" trên stack. Completion records (return, throw, normal) trong finally.
  • Stage 3 (Async) — try/catch với await. Promise rejection được catch như synchronous throw. finally trong async function.
  • Stage 8 (React) — Error Boundaries là try/catch ở component level. Catch render error và hiển thị fallback.
  • Stage 9/13 (Production) — Structured logging, error aggregation (Sentry), incident response. try/catch phải log đầy đủ context để trace production issue.
📴 Offline Mode — Content served from cache