ข้ามไปยังเนื้อหา

Error Boundaries

ถ้า component throw ระหว่าง render React — โดยตั้งใจ — จะ unmount ทั้ง tree แทนที่จะแสดง UI ที่เสียหาย error boundary คือ component ที่จับ error แบบนั้นใน subtree ของตัวเอง เพื่อให้ความล้มเหลวใน widget เดียวแสดง fallback ตรงนั้น แทนที่จะทำทั้งหน้าล่ม

error boundary ยังต้องเขียนเป็น class component เพราะพึ่ง lifecycle method สองตัวที่ไม่มี hook เทียบเท่า:

import { Component } from "react";
class ErrorBoundary extends Component {
state = { error: null };
// render phase: คืน state ใหม่เพื่อแสดง fallback ใน render ถัดไป
static getDerivedStateFromError(error) {
return { error };
}
// commit phase: side effect — log error ไปยัง reporting service ของคุณ
componentDidCatch(error, info) {
reportError(error, info.componentStack);
}
render() {
if (this.state.error) return this.props.fallback;
return this.props.children;
}
}

ใน production ส่วนใหญ่ใช้ library เล็ก ๆ ชื่อ react-error-boundary ที่ห่อของแบบนี้และเพิ่มกลไก reset — แต่กลไกข้างในก็คือด้านบนเป๊ะ ๆ

<ErrorBoundary fallback={<p>Something went wrong.</p>}>
<Widget /> {/* ถ้า Widget throw ระหว่าง render, fallback จะโชว์ตรงนี้ */}
</ErrorBoundary>

ตรงนี้คนเข้าใจผิดบ่อย error boundary จับ error ที่เกิด ระหว่าง rendering (และใน lifecycle method กับ constructor) ของ component ที่อยู่ ใต้ boundary แต่ ไม่ จับ:

  • event handler — error ใน onClick เป็น JS error ปกติ; ใช้ try/catch
  • asynchronous code — promise ที่ reject หรือ callback ของ setTimeout รันนอก render ของ React ดังนั้นไม่มี boundary ไหนเห็น
  • error ใน boundary เอง — boundary จับเฉพาะ error ที่อยู่ ใต้ ตัวเอง
  • server-side rendering error — จัดการแยกต่างหาก
flowchart TB
  eb["Error Boundary"] --> child["child render"]
  child -->|throw ระหว่าง render| caught["จับได้ → แสดง fallback"]
  handler["onClick throw"] -.ไม่ถูกจับ.-> handler2["ใช้ try/catch"]
  async["promise reject / setTimeout"] -.ไม่ถูกจับ.-> async2["จัดการเอง"]
error boundary จับอะไร

Suspense และ error boundary เสริมกัน: Suspense จัดการ state loading (child กำลังรอ) ส่วน error boundary จัดการ state failure (child throw — รวมถึง promise ที่ reject ซึ่งอ่านด้วย use()) รวมกันได้ทั้งสาม state ของ async UI แบบ declarative:

<ErrorBoundary fallback={<Error />}>
<Suspense fallback={<Spinner />}>
<Profile /> {/* loading → Spinner, error → Error, success → Profile */}
</Suspense>
</ErrorBoundary>

เมื่อ use() อ่าน promise ที่ reject React จะ propagate error นั้นไปยัง error boundary ที่ใกล้ที่สุด — ดังนั้น boundary เดียวกันที่จัดการ render crash ก็จัดการ data-fetch failure ด้วย

error boundary จับอะไร?
อันไหนที่ error boundary ไม่จับ?
ทำไม error boundary ยังต้องเขียนเป็น class component?
Suspense กับ error boundary ทำงานร่วมกันอย่างไร?