jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

JavaScript Error Handling & Resilience — try/catch Limits, Global Handlers, and Production Patterns

Senior deep-dive: Error object anatomy, what try/catch cannot catch, custom errors with cause, async patterns, global handlers, retries, and monitoring.

Vì sao xử lý lỗi là kỹ năng senior, không phải bài tập cú pháp

Frontend production hỏng theo cách nhàm chán, dự đoán được: script bên thứ ba throw trong timer, fetch reject không .catch, React child crash cả route, dashboard trắng không có stack trace trong báo cáo user.

Khoảng cách junior–senior không phải biết try/catch tồn tại — mà biết mỗi lớp bắt gì, gì lọt qua, và cách phục hồi mà không giấu lỗi.

Thử trực tiếp:

Mở demo đầy đủ:


Đối tượng Error — name, message, stack

Mọi giá trị throw trong JS có thể là bất cứ thứ gì — Production luôn throw instance Error (hoặc subclass) để tooling, monitoring, và instanceof hoạt động nhất quán.

const err = new Error("Payment gateway timeout");

console.log(err.name);    // "Error"
console.log(err.message); // "Payment gateway timeout"
console.log(err.stack);   // multi-line stack trace (engine-dependent)
Thuộc tínhMục đíchGhi chú
nameLoại lỗiBuilt-in tự set
messageTóm tắt cho ngườiAn toàn hiển thị UI nếu sanitize
stackChuỗi call-siteKhông chuẩn nhưng phổ biến; bị strip ở prod build
causeLỗi gốc chuỗiES2022 — xem dưới

Đừng throw string:

// ❌ No stack trace, breaks instanceof, monitoring can't classify
throw "something broke";

// ✅ Proper Error instance
throw new Error("something broke");

stack vô giá khi dev thường minify hoặc bỏ trong bundle production — đó là lý do source map và dịch vụ báo lỗi quan trọng.


Các loại Error built-in

JS có error typed cho failure phổ biến. Dùng type cụ th nhất để caller và monitor phân nhánh đúng.

TypeDùng khi
ErrorLỗi app chung
TypeErrorSai type / thao tác không hợp lệ
RangeErrorGiá trị ngoài phạm vi
ReferenceErrorBiến không hợp lệ
SyntaxErrorLỗi parse (hiếm runtime)
URIErrorBad encodeURI / decodeURI usage
AggregateErrorMultiple errors (e.g. Promise.any rejection)
function parseAge(input) {
  const n = Number(input);
  if (Number.isNaN(n)) throw new TypeError(`Expected numeric age, got "${input}"`);
  if (n < 0 || n > 150) throw new RangeError(`Age ${n} out of range`);
  return n;
}

DOM API thêm type như DOMException (name: "AbortError") — luôn check err.name, không chỉ message.


try / catch / finally — Phạm vi và giới hạn

Bọc code đồng bộ trong call stack hiện tại. Lớp phòng thủ đầu cho code bạn kiểm soát và có thể await.

function chargeCard(amount) {
  try {
    validateAmount(amount);
    return gateway.charge(amount);
  } catch (err) {
    logger.error("charge failed", { err, amount });
    throw err; // re-throw after logging {log rồi re-throw}
  } finally {
    hideSpinner(); // always runs {luôn chạy}
  }
}

try/catch bắt được gì

  • throw đồng bộ trong try
  • Lỗi từ gọi hàm đồng bộ trong try
  • Promise reject khi await trong async try

try/catch KHÔNG bắt — hiểu lầm số 1

Tình huốngVì sao lọtHandler đúng
setTimeout / setInterval callback throwChạy stack frame sauwindow.addEventListener('error')
Event listener throwGọi riêngGlobal error listener
Promise.reject() without .catchAsync; không trong sync stack.catch() or unhandledrejection
Errors in another microtask before your catch attachesRace thời điểmAlways chain .catch at creation
Cross-origin script errorsBrowser sanitize message/stackcrossorigin attribute + CORS headers
// ❌ Classic bug — try/catch looks like it should work
try {
  setTimeout(() => {
    throw new Error("This escapes try/catch");
  }, 0);
} catch (err) {
  // never runs {không bao giờ chạy}
}

// ✅ Catch inside the async callback, or use global handler
setTimeout(() => {
  try {
    riskyWork();
  } catch (err) {
    report(err);
  }
}, 0);

Mô hình tư duy: try/catchranh giới stack-frame, không phải ranh giới thời gian. Mọi thứ lên lịch sau cần handler riêng hoặc lưới an toàn global.


Custom Error class và cause của Error

Error theo domain giúp phân biệt “user không tồn tại” và “database sập” mà không parse string.

class ApiError extends Error {
  constructor(message, { statusCode, cause } = {}) {
    super(message, { cause });
    this.name = "ApiError";
    this.statusCode = statusCode;
  }
}

class NotFoundError extends ApiError {
  constructor(resource, id) {
    super(`${resource} ${id} not found`, { statusCode: 404 });
    this.name = "NotFoundError";
  }
}

ES2022 thêm option cause — Bọc lỗi tầng thấp mà không mất stack gốc:

async function loadProfile(userId) {
  try {
    const raw = await fetch(`/api/users/${userId}`).then((r) => r.json());
    return normalizeProfile(raw);
  } catch (err) {
    throw new ApiError("Failed to load profile", {
      statusCode: 502,
      cause: err, // preserves original TypeError, network error, etc.
    });
  }
}

// Later in monitoring:
// err.message → "Failed to load profile"
// err.cause.message → "Unexpected token < in JSON..."

Dùng instanceof ở biên — API throw ApiError, UI map NotFoundError → trang 404, lỗi lạ → fallback chung.


Async/Await vs Promise — Lan truyền lỗi

Cả hai mô hình cùng biểu diễn đồ thị failure async; khác chỗ gắn handler.

Promises — explicit .catch() chain

fetch("/api/data")
  .then((r) => {
    if (!r.ok) throw new ApiError("HTTP error", { statusCode: r.status });
    return r.json();
  })
  .then(renderChart)
  .catch((err) => {
    showErrorBanner(err);
    reportToSentry(err);
  });

Quên .catch ở bất kỳ mắt nào tạo unhandled rejection.

Async/await — try/catch reads like sync code

async function loadDashboard() {
  try {
    const res = await fetch("/api/data");
    if (!res.ok) throw new ApiError("HTTP error", { statusCode: res.status });
    const data = await res.json();
    renderChart(data);
  } catch (err) {
    showErrorBanner(err);
    reportToSentry(err);
  }
}

Bẫy quan trọng: async Hàm async trả Promise. Gọi không await hoặc .catch, lỗi vẫn lọt:

// ❌ Fire-and-forget — rejection becomes unhandled
loadDashboard();

// ✅ Explicit handling
loadDashboard().catch(reportToSentry);

// ✅ Or await at the call site
await loadDashboard();
PatternXử lý lỗiTốt cho
.then().catch()Per-chainCallback-style pipelines
async + try/catchPer-functionSequential async logic
Promise.allFirst rejection rejects allAll-or-nothing parallel
Promise.allSettledNever rejects; inspect eachPartial success bulk ops

Handler global — Lưới an toàn cuối

Khi lỗi lọt handler local, listener global là lớp cuối trên production. Dùng để log và báo cáo, không nuốt bug im lặng.

window.addEventListener('error')

Kích hoạt cho exception không bắt (gồm nhiều throw timer/listener) và lỗi tải resource.

window.addEventListener("error", (event) => {
  // Script error
  if (event.error) {
    reportToSentry(event.error);
    return;
  }

  // Resource load error (script, img, link…) — event.error is null
  if (event.target !== window) {
    reportResourceFailure({
      tag: event.target.tagName,
      src: event.target.src || event.target.href,
    });
    return;
  }

  reportToSentry(new Error(event.message));
});

Lỗi resource bubble error trên element — dùng capture phase trên window để bắt:

window.addEventListener(
  "error",
  (event) => {
    if (event.target instanceof HTMLScriptElement) {
      console.warn("Script failed:", event.target.src);
    }
  },
  true // capture phase {phase capture}
);

window.addEventListener('unhandledrejection')

Bắt Promise reject không có .catch cuối chuỗi:

window.addEventListener("unhandledrejection", (event) => {
  reportToSentry(event.reason);
  // Optionally prevent browser console noise in controlled scenarios
  // event.preventDefault();
});

Đừng chỉ dựa global. Là lưới, không phải chiến lược. Mọi entry point async vẫn cần xử lý local.

Legacy window.onerror

Vẫn hỗ trợ; trả true để ẩn UI lỗi browser ở một số engine. Ưu tiên addEventListener cho nhiều subscriber.

window.onerror = (message, source, lineno, colno, error) => {
  reportToSentry(error ?? new Error(String(message)));
  return false; // let browser also log {để browser log thêm}
};

Pattern an toàn — Degrade, Retry, Fallback

Resilience nghĩa là app vẫn chạy ở công suất giảm khi subsystem hỏng, không phải lỗi biến mất.

Graceful degradation

Hiện data cache, ẩn widget phụ, tắt tính năng tùy chọn:

async function renderRecommendations(userId) {
  try {
    const items = await fetchRecommendations(userId);
    mountWidget(items);
  } catch (err) {
    logger.warn("recommendations unavailable", { err });
    mountWidget([]); // empty state, not a crash {empty state, không crash}
  }
}

Retries with backoff

Retry lỗi tạm thời (5xx, mạng giật), không retry vĩnh viễn (4xx validation):

async function fetchWithRetry(fn, { maxAttempts = 3, baseDelay = 200 } = {}) {
  let lastError;
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      lastError = err;
      if (!isRetryable(err) || attempt === maxAttempts - 1) throw err;
      await delay(baseDelay * 2 ** attempt + Math.random() * 50);
    }
  }
  throw lastError;
}

Xem demo fetch resilience cho timeout + stale response:

Fallback values

const config = await loadRemoteConfig().catch(() => DEFAULT_CONFIG);

Fallback ổn cho đọc không quan trọng; không fallback im lặng trên ghi (thanh toán, xóa).

Result / Either pattern (brief)

Thay vì throw cho control flow, trả union phân biệt:

/** @returns {{ ok: true, value: T } | { ok: false, error: Error }} */
async function safeParseJson(response) {
  try {
    const value = await response.json();
    return { ok: true, value };
  } catch (err) {
    return { ok: false, error: err instanceof Error ? err : new Error(String(err)) };
  }
}

const result = await safeParseJson(res);
if (!result.ok) {
  showParseError(result.error);
  return;
}
useData(result.value);

Phổ biến trong TS khi muốn đường lỗi rõ trong type. Thư viện như neverthrow formalize.

React Error Boundaries (concept)

React không tự bắt lỗi event handler hay async trong cây. Error Boundary bắt lỗi render và lifecycle ở con:

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

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

  componentDidCatch(error, info) {
    reportToSentry(error, { componentStack: info.componentStack });
  }

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

Bọc cấp route hoặc widget; Kết hợp handler global cho mọi thứ ngoài render cycle React.


Báo cáo, Monitoring, và Source Map

User báo “hỏng” bằng screenshot, không stack trace. Việc của bạn là tái dạo context.

What to send to monitoring (Sentry, Datadog, etc.)

  • error.name, error.message, error.stack
  • error.cause chain (flatten recursively)
  • User/session id (never PII in messages)
  • Breadcrumbs: recent navigation, API calls, feature flags
  • Release version + environment (production, staging)
function reportToSentry(err, extra = {}) {
  Sentry.captureException(err, {
    extra: {
      ...extra,
      causeChain: flattenCause(err),
    },
    tags: {
      errorType: err.name,
    },
  });
}

function flattenCause(err) {
  const chain = [];
  let current = err;
  while (current?.cause instanceof Error) {
    chain.push({ name: current.cause.name, message: current.cause.message });
    current = current.cause;
  }
  return chain;
}

Source maps

Bundle production minify — stack trỏ a.b(c) không phải source. Upload source map lên monitoring trong CI để stack báo cáo deobfuscate về file và dòng thật.

Không public source map trên CDN trừ khi cố ý — Chúng lộ toàn bộ cây source.


Đừng nuốt lỗi

Xử lý lỗi tệ nhất là trông như thành công:

// ❌ Silent failure — debugging nightmare
try {
  await saveSettings(data);
} catch (err) {
  // nothing
}

// ❌ Log and pretend OK
try {
  return await fetchCriticalData();
} catch (err) {
  console.log(err);
  return null; // caller can't distinguish "no data" from "fetch failed"
}

// ✅ Log, report, and propagate or return explicit Result
try {
  return await fetchCriticalData();
} catch (err) {
  logger.error("critical fetch failed", { err });
  reportToSentry(err);
  throw err; // or return { ok: false, error: err }
}

catch rỗng và .catch(() => {}) nên bị reject trong review. Nếu phải suppress, ghi vì sao và emit metric.


Checklist quyết định cho code production

Tình huốngCách khuyến nghị
Sync validation in your functiontry/catch or early throw
Timer / event listener callbacktry/catch inside callback + global net
Promise chain you own.catch() at end or async try/catch
Fire-and-forget async.catch(report) on the returned Promise
Third-party script failureGlobal error listener + crossorigin
Widget / route crash (React)Error Boundary + fallback UI
Transient network failureRetry with backoff + circuit breaker
User-facing messageMap error type → safe copy; never raw stack

Điểm chính

  1. Throw instance Error, không string — giữ stack, name, cause.
  2. try/catch là đồng bộ — callback timer và Promise reject trần cần handler riêng.
  3. Custom error + cause giúp app nhiều lớp debug được.
  4. Handler global là lưới an toàn — không phải chiến lược chính.
  5. Resilience = degrade + retry + fallback, với failure hiện rõ.
  6. Báo cáo có contextkhông nuốt lỗi.

Lỗi không hiếm trên UI phân tán — là phần state machine bình thường. Thiết kế rõ ràng và pager on-call yên hơn.