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ính | Mục đích | Ghi chú |
|---|---|---|
name | Loại lỗi | Built-in tự set |
message | Tóm tắt cho người | An toàn hiển thị UI nếu sanitize |
stack | Chuỗi call-site | Không chuẩn nhưng phổ biến; bị strip ở prod build |
cause | Lỗi gốc chuỗi | ES2022 — 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.
| Type | Dùng khi |
|---|---|
Error | Lỗi app chung |
TypeError | Sai type / thao tác không hợp lệ |
RangeError | Giá trị ngoài phạm vi |
ReferenceError | Biến không hợp lệ |
SyntaxError | Lỗi parse (hiếm runtime) |
URIError | Bad encodeURI / decodeURI usage |
AggregateError | Multiple 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ộ trongtry- Lỗi từ gọi hàm đồng bộ trong
try - Promise reject khi
awaittrongasynctry
try/catch KHÔNG bắt — hiểu lầm số 1
| Tình huống | Vì sao lọt | Handler đúng |
|---|---|---|
setTimeout / setInterval callback throw | Chạy stack frame sau | window.addEventListener('error') |
| Event listener throw | Gọi riêng | Global error listener |
Promise.reject() without .catch | Async; không trong sync stack | .catch() or unhandledrejection |
| Errors in another microtask before your catch attaches | Race thời điểm | Always chain .catch at creation |
| Cross-origin script errors | Browser sanitize message/stack | crossorigin 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/catchlà ranh 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();
| Pattern | Xử lý lỗi | Tốt cho |
|---|---|---|
.then().catch() | Per-chain | Callback-style pipelines |
async + try/catch | Per-function | Sequential async logic |
Promise.all | First rejection rejects all | All-or-nothing parallel |
Promise.allSettled | Never rejects; inspect each | Partial 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
asyncvẫ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.stackerror.causechain (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ống | Cách khuyến nghị |
|---|---|
| Sync validation in your function | try/catch or early throw |
| Timer / event listener callback | try/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 failure | Global error listener + crossorigin |
| Widget / route crash (React) | Error Boundary + fallback UI |
| Transient network failure | Retry with backoff + circuit breaker |
| User-facing message | Map error type → safe copy; never raw stack |
Điểm chính
- Throw instance
Error, không string — giữstack,name,cause. try/catchlà đồng bộ — callback timer và Promise reject trần cần handler riêng.- Custom error +
causegiúp app nhiều lớp debug được. - Handler global là lưới an toàn — không phải chiến lược chính.
- Resilience = degrade + retry + fallback, với failure hiện rõ.
- Báo cáo có context và khô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.