Chương 07 · Error Handling & Defensive Patterns

Xử lý lỗi và các pattern phòng thủ

Code production khác code demo ở chỗ: luôn có thứ hỏng — mạng rớt, API trả 500, user nhập rác, file mất. Chương này dạy bạn 3 việc: (1) throw đúng cách khi gặp lỗi, (2) catch đúng nơi để không nuốt im lặng, (3) thiết kế code phòng thủ sao cho mỗi error có một "vùng" xử lý rõ ràng — không làm cả app sập.

Độ dài: ~800 dòng Bài tập: 5 Quiz: 7 Prerequisites: Chương 1-6
🎯 Mục tiêu chương
  • Hiểu Error object và các built-in subclass (TypeError, RangeError, SyntaxError, ReferenceError).
  • Master try/catch/finally, kể cả khi mix với async/await.
  • Tự tạo custom error class để phân biệt loại lỗi.
  • Biết các anti-pattern phổ biến: swallow error, log không re-throw.
  • Hiểu Result type — pattern lấy cảm hứng từ Rust, ép caller phải xử lý lỗi.
  • Thiết kế fail-fast, defensive copy, error boundary ở UI.
  • Cài global error handler ở browser và Node — lớp phòng cuối.

1. Error object trong JavaScript

JS có một type built-in tên Error. Khi có sự cố runtime (chia cho 0 trên BigInt, gọi method của undefined, parse JSON sai...), JS tự throw một instance của Error (hoặc subclass). Developer cũng có thể chủ động throw.

Anatomy of an Error
const err = new Error('Something broke');

err.name;     // 'Error'      — tên loại
err.message;  // 'Something broke' — chuỗi cho người đọc
err.stack;    // 'Error: Something broke\n    at :1:13\n ...' — vị trí throw

// throw nó để dừng chương trình (cho đến khi có catch)
throw err;

1.1. Các subclass built-in

JS có sẵn nhiều subclass của Error. Mỗi cái mô tả một loại lỗi khác nhau.

ClassKhi nào sinh raVí dụ trigger
ErrorGeneric — base classnew Error('msg')
TypeErrorSai type khi vận hànhnull.foo, gọi non-function
RangeErrorGiá trị vượt range hợp lệnew Array(-1), recursion quá sâu
SyntaxErrorCode không parse đượcJSON.parse('xx'), eval string sai
ReferenceErrorTruy cập biến chưa khai báo / TDZconsole.log(notDeclared)
URIErrorURI function lỗidecodeURIComponent('%')
EvalErrorLịch sử (ít dùng)
AggregateErrorGom nhiều error (Promise.any reject hết)Promise.any([reject, reject])
Built-in errors in action
// TypeError
const x = null;
x.foo;  // TypeError: Cannot read properties of null (reading 'foo')

// RangeError
new Array(-1);  // RangeError: Invalid array length

// SyntaxError (runtime, từ JSON.parse)
JSON.parse('not json');  // SyntaxError: Unexpected token 'o'

// ReferenceError
console.log(notDeclared);  // ReferenceError: notDeclared is not defined

Vì tất cả subclass đều kế thừa Error, bạn có thể check loại lỗi bằng instanceof:

try {
  JSON.parse('xx');
} catch (e) {
  if (e instanceof SyntaxError) {
    console.log('JSON sai cú pháp');
  } else if (e instanceof Error) {
    console.log('Lỗi khác:', e.message);
  }
}

1.2. Anti-pattern: throw 'string'

JavaScript cho phép throw bất kỳ value gì — string, number, object thường. Không nên. Khi throw string, bạn mất stack trace, e.message, instanceof Error.

🔥 Đừng làm thế này
throw 'Something broke';   // ❌ không có stack, không có name
throw 404;                  // ❌ developer phải đoán nghĩa
throw { code: 404 };          // ❌ không là Error → instanceof fail

// ✅ Đúng:
throw new Error('Something broke');

Quy tắc: luôn throw một instance của Error (hoặc subclass). Tự đặt rule này trong ESLint với no-throw-literal.

2. try / catch / finally

Cấu trúc cơ bản để bắt lỗi synchronous. Khi code trong try throw, control nhảy ngay sang catch. finally chạy luôn luôn — bất kể có lỗi hay không, kể cả khi catch re-throw.

Basic try/catch/finally
function divide(a, b) {
  if (b === 0) throw new Error('Division by zero');
  return a / b;
}

try {
  const r = divide(10, 0);
  console.log('kết quả:', r);     // không chạy
} catch (e) {
  console.error('lỗi:', e.message); // 'lỗi: Division by zero'
} finally {
  console.log('luôn chạy');          // chạy
}

2.1. finally chạy khi nào

Ngắn gọn: mọi lúc. Kể cả khi try hoặc catchreturn hoặc re-throw.

finally — luôn chạy
function readFile(path) {
  const file = openFile(path);
  try {
    return file.readAll();          // có return — finally vẫn chạy
  } catch (e) {
    throw new Error(`Read failed: ${e.message}`); // re-throw — finally vẫn chạy
  } finally {
    file.close();                  // ✅ luôn đóng file
  }
}
🧠 Mental model — RAII bằng finally

Trong C++ có RAII: object tự dọn resource khi out of scope (destructor). JS không có destructor, nhưng finally là cách thủ công: "resource mở trong try, dọn trong finally". Áp dụng cho file handle, DB connection, lock, timer, event listener.

2.2. catch không cần binding (ES2019)

Trước ES2019, bạn phải viết catch (e) dù không dùng e. ES2019 cho phép bỏ luôn:

// Cũ:
try { JSON.parse(input); }
catch (e) { return null; }   // `e` không dùng nhưng vẫn phải khai báo

// ES2019:
try { JSON.parse(input); }
catch { return null; }       // ✅ không cần (e)

3. Custom Error class — phân biệt loại lỗi

Trong code thật, bạn cần phân biệt: lỗi validation (user gõ sai), lỗi network (API timeout), lỗi authorization (token expired)... Mỗi loại có cách xử lý khác. Cách clean nhất là tạo subclass của Error.

ValidationError
class ValidationError extends Error {
  constructor(field, message) {
    super(message);          // gọi Error constructor — set this.message
    this.name = 'ValidationError';  // override để log đẹp + instanceof
    this.field = field;       // thêm thông tin nghiệp vụ
  }
}

class NotFoundError extends Error {
  constructor(resource) {
    super(`${resource} not found`);
    this.name = 'NotFoundError';
    this.resource = resource;
  }
}

// Sử dụng
function getUser(id) {
  if (typeof id !== 'number') {
    throw new ValidationError('id', 'id phải là number');
  }
  const u = db.find(id);
  if (!u) throw new NotFoundError('User');
  return u;
}

// Caller dispatch theo loại
try {
  getUser('abc');
} catch (e) {
  if (e instanceof ValidationError) {
    ui.showFieldError(e.field, e.message);
  } else if (e instanceof NotFoundError) {
    ui.show404();
  } else {
    ui.showGenericError();
    reportToSentry(e);
  }
}
💡 Luôn set this.name

Mặc định this.name kế thừa từ Error nên sẽ là 'Error', không phải tên class mới. Khi log err.toString() sẽ ra "Error: msg" thay vì "ValidationError: msg". Luôn override this.name = 'TenClass' trong constructor.

4. try/catch với async/await

Đây là điểm hay nhất của async/await so với .then().catch(): bạn dùng được cùng một cấu trúc try/catch cho cả sync và async.

async try/catch
async function loadUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (e) {
    console.error('loadUser failed:', e);
    throw e;   // re-throw cho caller cao hơn
  }
}

So sánh với .then().catch():

// Promise chain
function loadUserChain(id) {
  return fetch(`/api/users/${id}`)
    .then(res => {
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return res.json();
    })
    .catch(e => {
      console.error('loadUser failed:', e);
      throw e;
    });
}
⚖️ try/catch async bắt cả 2 loại

Trong block try { await foo(); }, catch sẽ bắt:

  1. Lỗi sync ném ra trước khi Promise được tạo (vd: foo sai signature).
  2. Promise reject trả về từ await foo().

Đó là lý do nó rất gọn. Nhưng nhớ: try/catch không bắt được error xảy ra trong callback setTimeout, setInterval, hoặc Promise nào không có await.

Bẫy quên await
async function bad() {
  try {
    foo();   // ❌ quên await — Promise reject không bị bắt
  } catch (e) {
    console.log('never reaches here');
  }
}

async function good() {
  try {
    await foo();   // ✅
  } catch (e) {
    console.log('caught', e);
  }
}

5. Anti-pattern: nuốt lỗi (swallow)

Đây là tội lỗi số 1 trong code production. try/catch không có nội dung — error biến mất, bug ẩn đi mãi mãi.

🔥 Đừng làm thế này — silent swallow
// ❌ Tệ — không ai biết có lỗi xảy ra
try {
  const data = JSON.parse(input);
  save(data);
} catch {}

// ❌ Cũng tệ — log nhưng không re-throw, UI thinks success
async function submit() {
  try {
    await api.save(data);
  } catch (e) {
    console.log(e);  // log xong... rồi sao?
  }
  return { ok: true };  // UI hiện "lưu thành công" dù lỗi 🔥
}

Bug này cực kỳ phổ biến. User báo "tôi bấm save mà data không lưu" — bạn không thấy gì trong log production (vì console.log không gửi đi đâu). Hàng giờ debug.

Quy tắc: nếu bạn catch, bạn phải làm ít nhất 1 trong 3 việc sau:

  1. Re-throw — không xử lý được, để caller xử lý: throw e;
  2. Recover — có fallback rõ ràng: return defaultValue;
  3. Báo cáo error state — return {ok: false, error: e} để caller biết.
Fix swallow — 3 cách
// ✅ Cách 1: re-throw
try { risky(); }
catch (e) {
  logger.error('risky failed', e);
  throw e;
}

// ✅ Cách 2: recover với default
function safeParse(s) {
  try { return JSON.parse(s); }
  catch { return {}; }   // chấp nhận parse fail → object rỗng
}

// ✅ Cách 3: return error state (Result type)
async function submit() {
  try {
    await api.save(data);
    return { ok: true };
  } catch (e) {
    return { ok: false, error: e };
  }
}

6. Result type pattern — học từ Rust

Rust không có exception. Mọi hàm có thể fail đều trả về Result<T, E> — kiểu union giữa thành công + valuethất bại + error. Lợi ích: compiler ép caller xử lý. JS không có compiler ép, nhưng pattern vẫn rất giá trị: caller thấy hàm có thể fail.

Result type cơ bản
// Type: { ok: true, value: T } | { ok: false, error: E }

function ok(value)  { return { ok: true,  value }; }
function err(error) { return { ok: false, error }; }

function parseAge(input) {
  const n = Number(input);
  if (Number.isNaN(n)) return err('not a number');
  if (n < 0) return err('negative');
  if (n > 150) return err('too large');
  return ok(n);
}

// Caller buộc phải check .ok trước khi truy cập value/error
const r = parseAge(input);
if (r.ok) {
  console.log('Tuổi:', r.value);
} else {
  console.log('Lỗi:', r.error);
}
⚖️ Throw vs Result — chọn cái nào
  • Throw khi lỗi là exceptional — bất thường, ngoài dự tính. Vd: out of memory, bug logic, DB chết.
  • Result khi lỗi là expected — nằm trong "happy path" mở rộng. Vd: validate user input, parse, lookup không tìm thấy.

Rule of thumb: nếu bạn biết trước sẽ có case fail (parse JSON từ user), dùng Result. Nếu fail là "không nên xảy ra", throw.

7. Fail-fast pattern

Triết lý: chết ngay từ đầu hàm còn hơn để code chạy nửa chừng rồi mới phát hiện. Validate input ở dòng đầu, throw nếu sai. Tránh state nửa-vời.

Fail-fast vs lazy check
// ❌ Lazy — check muộn, có thể đã ghi DB rồi mới phát hiện
async function createOrder(items, userId) {
  await db.orders.insert({ items, userId });
  await billing.charge(userId, sum(items));   // quá muộn nếu items rỗng
}

// ✅ Fail-fast — validate trước, chỉ ghi khi chắc
async function createOrder(items, userId) {
  if (!Array.isArray(items) || items.length === 0) {
    throw new ValidationError('items', 'phải có ít nhất 1 sản phẩm');
  }
  if (typeof userId !== 'number') {
    throw new ValidationError('userId', 'phải là number');
  }

  await db.orders.insert({ items, userId });
  await billing.charge(userId, sum(items));
}
🧠 Tại sao fail-fast tốt
  • Half-done state là kẻ thù. Order đã insert, nhưng charge fail — DB dirty.
  • Stack trace gần root cause. Throw sớm = log ngay nơi lỗi thực sự.
  • Code reader dễ hiểu. Đọc 3 dòng đầu hàm là biết preconditions.

8. Defensive copy

Khi hàm nhận object/array từ caller, đừng mutate nó trực tiếp. Caller có thể không biết bạn sửa, dẫn đến bug "sao state ở chỗ khác cũng đổi?". Pattern: clone trước khi mutate.

Defensive copy
// ❌ Mutate input — surprise bug
function addTimestamp(obj) {
  obj.timestamp = Date.now();
  return obj;
}

const user = { name: 'An' };
addTimestamp(user);
console.log(user);  // { name: 'An', timestamp: ... } 🔥 caller bị mutate

// ✅ Defensive copy — clone trước
function addTimestamp(obj) {
  return { ...obj, timestamp: Date.now() };
}

// Cho object lồng nhau, dùng structuredClone (hiện đại) hoặc deepClone lib
function deepUpdate(obj, path, value) {
  const clone = structuredClone(obj);
  _.set(clone, path, value);
  return clone;
}
💡 Pattern liên quan

Defensive copy là nền của immutable data — triết lý "không bao giờ sửa, luôn tạo bản mới". Đây là cách React, Redux, và các state library hoạt động. Sẽ học sâu ở chương sau và Pillar OOP/FP.

9. Global error handler — lớp phòng cuối

Dù bạn try/catch kỹ, vẫn có error lọt — uncaught exception, unhandled promise rejection. Khi đó program có thể crash. Bạn cần một global handler để log + cứu vãn graceful.

9.1. Browser

Browser global handlers
// Bắt sync error không catch
window.addEventListener('error', (event) => {
  console.error('Uncaught error:', event.error);
  reportToServer({
    type: 'error',
    message: event.message,
    filename: event.filename,
    lineno: event.lineno,
    stack: event.error?.stack,
  });
});

// Bắt promise reject không catch
window.addEventListener('unhandledrejection', (event) => {
  console.error('Unhandled rejection:', event.reason);
  reportToServer({
    type: 'unhandledrejection',
    reason: String(event.reason),
    stack: event.reason?.stack,
  });
});

9.2. Node.js

Node global handlers
process.on('uncaughtException', (err) => {
  logger.fatal({ err }, 'Uncaught exception');
  // Process ở state không chắc chắn — gracefully exit
  process.exit(1);
});

process.on('unhandledRejection', (reason) => {
  logger.error({ reason }, 'Unhandled promise rejection');
  // Từ Node 15+: mặc định cũng exit. Có thể override.
});
⚠️ Đây là lớp phòng CUỐI

Đừng dùng global handler thay cho try/catch. Mục đích của nó: log để bạn biết có bug nào lọt, sau đó graceful shutdown (Node) hoặc show fallback UI (browser). Không phải để "tiếp tục như bình thường" sau khi crash — state có thể đã hỏng.

10. Error boundary trong UI framework

Trong UI declarative (React, Vue, Flutter), một component lỗi có thể làm crash cả cây render. Framework cung cấp error boundary — một component "khoanh vùng" lỗi, fallback UI thay vì trắng màn hình.

React ErrorBoundary
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, info) {
    reportToSentry(error, { extra: info });
  }

  render() {
    if (this.state.hasError) {
      return <FallbackUI error={this.state.error} />;
    }
    return this.props.children;
  }
}

// Bọc khu vực rủi ro:
<ErrorBoundary>
  <UserDashboard />
</ErrorBoundary>

Flutter có khái niệm tương đương: ErrorWidget.builder để override widget hiển thị khi build lỗi. Sẽ học sâu ở Flutter Chương 6 (Widget) và Chương 10 (Error handling).

💡 Thiết kế "error zones"

Đừng đặt 1 ErrorBoundary ở root rồi xong. Đặt nhiều boundary nhỏ ở các vùng độc lập (sidebar, content, footer). Khi widget X lỗi, chỉ vùng X hỏng — phần còn lại app vẫn dùng được. Đây là nguyên tắc blast radius containment.

11. Log error đúng cách

Lỗi phổ biến: console.log(JSON.stringify(err)) → ra '{}'Error có property name, message, stacknon-enumerable. JSON.stringify bỏ qua chúng.

Log Error đúng vs sai
const err = new Error('oops');

// ❌ Tệ — mất hết info
JSON.stringify(err);                  // '{}' 🔥

// ✅ Đúng — extract field tay
JSON.stringify({
  name: err.name,
  message: err.message,
  stack: err.stack,
});

// ✅ Hoặc dùng helper:
function serializeError(err) {
  if (!(err instanceof Error)) return { value: String(err) };
  return {
    name: err.name,
    message: err.message,
    stack: err.stack,
    ...err,   // spread bao luôn custom properties
  };
}

// ✅ Console.log nhận Error object trực tiếp — devtools format đẹp
console.error(err);   // hiển thị stack + clickable line
💡 Sentry / DataDog ở production

Đừng tự build error reporting. Dùng Sentry, DataDog RUM, hoặc Bugsnag — chúng tự capture stack, breadcrumb (action user trước khi crash), source map (de-minify), release version. Cài 5 phút, tiết kiệm 5 tuần debug.

12. Tư duy phòng thủ tổng thể

Tổng hợp 4 nguyên tắc của chương:

Nguyên tắcVùng áp dụngPattern cụ thể
Fail fastĐầu hàm, đầu requestValidate input → throw ValidationError
Result over throw cho expected errorParse, validate, lookupReturn {ok, value/error}
Defensive copyHàm public, library APISpread ..., structuredClone
Blast radiusUI tree, request handlerErrorBoundary, middleware
Last-resort loggingToàn appGlobal handler + Sentry

Bài tập

Bài 1 — ApiError với metadata

Viết class ApiError extends Error có thêm statusCode (number) và endpoint (string). Sau đó viết function callApi(url) dùng fetch, throw ApiError khi response không 2xx.

Đáp án mẫu
class ApiError extends Error {
  constructor(statusCode, endpoint, message) {
    super(message ?? `API ${endpoint} failed with ${statusCode}`);
    this.name = 'ApiError';
    this.statusCode = statusCode;
    this.endpoint = endpoint;
  }
}

async function callApi(url) {
  const res = await fetch(url);
  if (!res.ok) {
    throw new ApiError(res.status, url);
  }
  return await res.json();
}

// Sử dụng
try {
  const data = await callApi('/api/users/999');
} catch (e) {
  if (e instanceof ApiError && e.statusCode === 404) {
    ui.show404();
  } else {
    throw e;
  }
}

Bài 2 — safeFetch trả Result type

Viết safeFetch(url) trả {ok: true, value: data} khi thành công, {ok: false, error} khi fail. Cover 3 case: network error, non-2xx, JSON parse fail. Không bao giờ throw.

Đáp án mẫu
async function safeFetch(url) {
  let res;
  try {
    res = await fetch(url);
  } catch (e) {
    return { ok: false, error: { kind: 'network', cause: e } };
  }

  if (!res.ok) {
    return { ok: false, error: { kind: 'http', status: res.status } };
  }

  try {
    const data = await res.json();
    return { ok: true, value: data };
  } catch (e) {
    return { ok: false, error: { kind: 'parse', cause: e } };
  }
}

// Caller
const r = await safeFetch('/api/users');
if (r.ok) {
  render(r.value);
} else switch (r.error.kind) {
  case 'network': ui.showOffline(); break;
  case 'http':    ui.showHttpError(r.error.status); break;
  case 'parse':   ui.showServerError(); break;
}

Lưu ý: error kind là string literal → ở TypeScript sẽ dùng discriminated union (chương 9).

Bài 3 — withRetry có điều kiện

Viết withRetry(fn, retries, shouldRetry):

  • fn: async function không tham số.
  • retries: số lần retry tối đa.
  • shouldRetry(error): predicate quyết định có retry không (vd: chỉ retry network error, không retry 4xx).

Nếu vượt số retry, throw error cuối cùng.

Đáp án mẫu
async function withRetry(fn, retries, shouldRetry) {
  let lastError;
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      return await fn();
    } catch (e) {
      lastError = e;
      if (attempt === retries || !shouldRetry(e)) throw e;
      // Backoff đơn giản: 100ms, 200ms, 400ms...
      await new Promise(r => setTimeout(r, 100 * 2 ** attempt));
    }
  }
  throw lastError;
}

// Sử dụng — chỉ retry khi network hoặc 5xx
const data = await withRetry(
  () => callApi('/api/data'),
  3,
  (e) => e instanceof ApiError ? e.statusCode >= 500 : true
);

Bài 4 — Tìm bug nuốt lỗi

Code dưới có 1 bug khiến lỗi từ saveUser bị nuốt. Tìm và sửa.

async function register(form) {
  try {
    validate(form);
    saveUser(form);   // hàm async
    return { ok: true };
  } catch (e) {
    return { ok: false, error: e.message };
  }
}
Đáp án

Bug: thiếu await trước saveUser(form). Function async return Promise — nếu không await, Promise reject ở ngoài try/catch, trở thành unhandled rejection. register sẽ return {ok: true} dù save fail.

async function register(form) {
  try {
    validate(form);
    await saveUser(form);   // ✅ thêm await
    return { ok: true };
  } catch (e) {
    return { ok: false, error: e.message };
  }
}

ESLint rule @typescript-eslint/no-floating-promises hoặc require-await sẽ catch bug này tự động.

Bài 5 — Global error logger vào localStorage

Setup 2 listener window.addEventListener('error')'unhandledrejection'. Mỗi error append vào array trong localStorage key 'errorLog' (max 50 entry, xoá entry cũ nhất khi đầy). Mỗi entry gồm: timestamp, type, message, stack.

Test: trigger 1 sync error (null.foo) và 1 async error (Promise.reject('x')), kiểm tra log.

Đáp án mẫu
const MAX_LOG = 50;
const LOG_KEY = 'errorLog';

function appendLog(entry) {
  let log = [];
  try {
    log = JSON.parse(localStorage.getItem(LOG_KEY) ?? '[]');
  } catch { log = []; }
  log.push(entry);
  if (log.length > MAX_LOG) log = log.slice(-MAX_LOG);
  localStorage.setItem(LOG_KEY, JSON.stringify(log));
}

window.addEventListener('error', (event) => {
  appendLog({
    timestamp: new Date().toISOString(),
    type: 'error',
    message: event.message,
    stack: event.error?.stack ?? null,
  });
});

window.addEventListener('unhandledrejection', (event) => {
  const reason = event.reason;
  appendLog({
    timestamp: new Date().toISOString(),
    type: 'unhandledrejection',
    message: reason instanceof Error ? reason.message : String(reason),
    stack: reason?.stack ?? null,
  });
});

// Trigger để test:
setTimeout(() => { null.foo; }, 100);            // sync
Promise.reject(new Error('async fail'));            // async

// Kiểm tra:
console.log(JSON.parse(localStorage.getItem(LOG_KEY)));

Lưu ý: localStorage dung lượng giới hạn (~5MB) — nhớ cap số entry. Production nên gửi log lên server (Sentry / endpoint riêng) thay vì lưu local.

Quiz

Q1

throw 'oops' khác throw new Error('oops') ở chỗ nào?

Xem đáp án
✓ Đáp án

throw 'oops' ném string trần — không có stack trace, không có .message, instanceof Error trả false. Catcher khó debug và khó dispatch theo loại. Luôn dùng new Error(...) (hoặc subclass) — ESLint rule no-throw-literal bắt vi phạm tự động.

Q2

try { await foo() } catch(e) { ... }catch sẽ bắt loại lỗi nào?

Xem đáp án
✓ Đáp án

Cả hai: (1) lỗi sync throw trước khi Promise được tạo (vd: gọi foo trong điều kiện sai), và (2) Promise reject trả về từ await foo(). Đây là điểm hay nhất của async/await — bạn dùng cùng một cấu trúc cho cả sync và async. Không bắt được: error trong callback của setTimeout/event listener, hoặc Promise quên await.

Q3

finally có chạy nếu catch re-throw không?

Xem đáp án
✓ Đáp án

Có. finally chạy luôn luôn — bất kể try success, catch được kích hoạt, catch re-throw, hay có return sớm. Đây là đặc tính cốt lõi: finally đảm bảo cleanup (đóng file, release lock) chạy trong mọi trường hợp.

Q4

Trong try { risky(); } catch(e) { logger.log(e); } — error có propagate lên hàm gọi risky không?

Xem đáp án
✓ Đáp án

Không. catch đã "tiêu thụ" error — control flow tiếp tục bình thường sau block. Để propagate, phải throw e; (hoặc throw error mới) trong catch. Đây là anti-pattern phổ biến: log nhưng không re-throw → caller tưởng thành công.

Q5

JSON.parse('xx') throw cái gì, và instanceof Error có trả true không?

Xem đáp án
✓ Đáp án

Throw một SyntaxError. Vì SyntaxError kế thừa Error, cả hai check đều true:

try { JSON.parse('xx'); }
catch (e) {
  e instanceof SyntaxError;  // true
  e instanceof Error;        // true (kế thừa)
}

Đó là lý do catch (e) { if (e instanceof Error) ... } rất an toàn cho mọi error built-in.

Q6

Khi nào nên throw error, khi nào nên return error?

Xem đáp án
✓ Đáp án

Throw cho lỗi exceptional — bất ngờ, ngoài happy path, không nên xảy ra (out-of-memory, bug logic, network down, DB chết). Return Result ({ok, value/error}) cho lỗi expected — nằm trong kịch bản dự kiến, caller chắc chắn phải xử lý (parse user input, validate form, lookup không tìm thấy). Pattern Result lấy cảm hứng từ Rust, ép caller check trước khi truy cập value.

Q7

Promise.reject(new Error('x')) không có .catch() đi kèm — chuyện gì xảy ra?

Xem đáp án
✓ Đáp án

Trigger event 'unhandledrejection':

  • Browser: window.addEventListener('unhandledrejection', ...) nhận event. Mặc định console log warning, app tiếp tục chạy.
  • Node.js: process.on('unhandledRejection', ...) nhận event. Từ Node 15+ process exit với mã 1 (mặc định) nếu không handler — coi như crash.

Luôn handle Promise reject: .catch(), try/catch với await, hoặc đảm bảo có global handler. Trong test, unhandled rejection thường làm test fail.

Tổng kết

Sau chương 7, bạn nên master:

  • Error object: cấu trúc name/message/stack, các subclass built-in, luôn throw instance của Error.
  • try/catch/finally: cú pháp đầy đủ, finally luôn chạy, catch không cần binding (ES2019).
  • Custom error class: extends Error, set this.name, thêm field nghiệp vụ — dispatch bằng instanceof.
  • async/await + try/catch: bắt cả sync throw và Promise reject. Cẩn thận quên await.
  • Anti-pattern: nuốt lỗi (catch rỗng), log không re-throw. Nếu catch, phải re-throw / recover / báo error state.
  • Result type: pattern Rust — return {ok, value/error} cho expected error.
  • Fail-fast + defensive copy: validate đầu hàm, clone object trước khi mutate.
  • Global handler + Error boundary: lớp phòng cuối ở browser/Node và UI framework.
  • Log đúng cách: không JSON.stringify Error (mất stack), dùng Sentry ở production.

Kết nối

  • Chương 6 (Async) — error trong Promise chain (.catch) liên kết trực tiếp với try/catch async ở đây.
  • Chương 8 (Modules) — module boundary là nơi tự nhiên đặt error policy: validate input, expose typed error.
  • Chương 9 (TypeScript) — Result type lên một level với discriminated union + exhaustive check. Error kind trở thành type literal compiler ép xử lý đủ case.
  • Dart Chương 5 — exception trong Dart có syntax tương tự (try/on/catch/finally), thêm rethrow keyword. Sound null safety giảm hẳn TypeError.
  • Flutter Chương 6 (Widgets)ErrorWidget.builder để override khi build error.
  • Flutter Chương 10 (Error handling)FlutterError.onError, PlatformDispatcher.instance.onError, integrate Sentry/Crashlytics.