30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

Error Boundary で React アプリ内のエラーを捕捉する

Error Boundary は React のv16から導入された機能で、これを使うとコンポーネント内で発生したエラーをキャッチすることが出来る。

主に、エラー用のUIを表示したり、エラーを記録したりすることに使われる。
前者にはstatic getDerivedStateFromErrorというメソッドを、後者にはcomponentDidCatchというメソッドを用いる。
Error Boundary のためのクラスコンポーネントを作り、そこにこれらのメソッドを定義して使う。

この記事では、バージョン16.8.6で動作確認している。

動作確認用のアプリの用意

以下の内容でアプリを作る。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Template htmlfile</title>
</head>
<body>
  <p>↓この下に React アプリが表示される↓</p>
  <div id="app"></div>
  <p>↑この上に React アプリが表示される↑</p>
</body>
</html>
import React from 'react';
import ReactDOM from 'react-dom';

const AquaChild = () => (
  <div style={{backgroundColor: 'white'}}>aqua child</div>
);

const Aqua = () => (
  <div style={{backgroundColor: 'aqua', padding: '10px'}}>
    aqua
    <AquaChild />
  </div>
);
const Lime = () => <div style={{backgroundColor: 'lime'}}>lime</div>;

const App = () => (
  <>
    <Aqua />
    <Lime />
  </>
);

ReactDOM.render(<App />, document.querySelector('#app'));

これをビルドすると以下のようなUIを持ったアプリが出来るので、これを対象に検証していく。

f:id:numb_86:20190615221816p:plain

エラー用のUIを表示させる

React はv16から、発生したエラーがキャッチされなかった場合、コンポーネントツリー全体をアンマウントするようになった。
公式ドキュメントによればこれは、壊れたUIを表示することは何も表示しないことよりも悪いことである、という考えによるもの。

試しに、先程作ったサンプルでエラーを発生させてみる。

 const Aqua = () => (
   <div style={{backgroundColor: 'aqua', padding: '10px'}}>
-    aqua
+    aqua{x}
     <AquaChild />
   </div>
 );

Aquaのなかでxを参照しているが、xは存在しない変数なのでReferenceErrorが発生する。
この結果、アプリ全体が表示されなくなる。

f:id:numb_86:20190615221856p:plain

エラーが発生したAquaだけでなく、Limeも含めたコンポーネントツリー全体がアンマウントされているのが分かる。

static getDerivedStateFromErrorを使ってエラーをキャッチすることで、ツリー全体がアンマウントされるのを防ぎ、適切なUIを表示させることが出来る。

Error Boundary を使うため、以下のコンポーネントを作成した。

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = {hasError: false};
  }

  static getDerivedStateFromError(error) {
    console.log(error instanceof Error);
    return {hasError: true};
  }

  render() {
    const {state, props} = this;
    if (state.hasError) {
      return (
        <div style={{backgroundColor: 'orange'}}>
          背景がオレンジのコンポーネントは ErrorBoundary
          によって表示されているエラー用のコンポーネントです。
        </div>
      );
    }
    return props.children;
  }
}

static getDerivedStateFromErrorの引数で、キャッチしたエラーオブジェクトを取得できる。

そしてこのコンポーネントで、AquaLimeをそれぞれラップする。

 const App = () => (
   <>
-    <Aqua />
-    <Lime />
+    <ErrorBoundary>
+      <Aqua />
+    </ErrorBoundary>
+    <ErrorBoundary>
+      <Lime />
+    </ErrorBoundary>
   </>
 );

こうすることで、AquaLimeのなかでエラーが発生した際に、それをキャッチできる。

この状態でページを表示すると、今度は以下のようになっている。

f:id:numb_86:20190615221920p:plain

Aquaで発生したエラーをキャッチしてエラー用のUIが表示されている。
その一方で、Limeではエラーが発生していないので、そのまま表示されている。

このように、Error Boundary を上手く使うことでアプリ全体がクラッシュするのを防ぎ、適切なUIをユーザーに提供できるようになる。

Error Boundary の直下ではなくもっと深い階層でエラーが発生しても、問題なくキャッチできる。

 const AquaChild = () => (
-  <div style={{backgroundColor: 'white'}}>aqua child</div>
+  <div style={{backgroundColor: 'white'}}>aqua child{x}</div>
 );
 
 const Aqua = () => (
   <div style={{backgroundColor: 'aqua', padding: '10px'}}>
-    aqua{x}
+    aqua
     <AquaChild />
   </div>
 );

AquaではなくAquaChildでエラーを発生させても、先程と同じ表示になる。

f:id:numb_86:20190615221920p:plain

コンポーネントのなかでエラーが発生すると、ツリーを上に辿っていき、一番最初に到達した ErrorBoundary がエラーをキャッチする仕組みになっている。つまり、JavaScript のcatch{}と同じような挙動である。
最後までキャッチされなかった場合は、既に述べたようにツリー全体がアンマウントされる。

Error Boundary の対象になるのは、配下のコンポーネントで発生したエラーのみ。
Error Boundary 自身のなかで発生したエラーは、キャッチすることが出来ない。
例えば以下のようにすると、キャッチできずにツリー全体がアンマウントされてしまう。

 const AquaChild = () => (
-  <div style={{backgroundColor: 'white'}}>aqua child{x}</div>
+  <div style={{backgroundColor: 'white'}}>aqua child</div>
 );

const App = () => (
   <>
     <ErrorBoundary>
       <Aqua />
+      <div>{x}</div>
     </ErrorBoundary>
     <ErrorBoundar

このエラーをキャッチしたければ、Appをラップする必要がある。

-ReactDOM.render(<App />, document.querySelector('#app'));
+ReactDOM.render(
+  <ErrorBoundary>
+    <App />
+  </ErrorBoundary>,
+  document.querySelector('#app')
+);

そうするとAppの代わりにエラー用のUIが表示されるようになる。

f:id:numb_86:20190615221954p:plain

エラーを記録する

エラーをログに残したり、エラー監視サービスなどに送信したりする場合は、Error Boundary にcomponentDidCatchメソッドを定義して、そのなかで行う。

     return {hasError: true};
   }
 
+  componentDidCatch(error, info) {
+    console.log(error);
+    console.log(info.componentStack);
+  }
+
   render() {
     const {state, props} =

第一引数で、キャッチしたエラーオブジェクトを取得できる。
第二引数のinfocomponentStackを持っており、ここには、エラーが発生したコンポーネントのスタックトレースが入っている。

以下は、AquaChildでエラーが発生した際のスタックトレース。

    in AquaChild (at src/index.js:13)
    in div (at src/index.js:11)
    in Aqua (at src/index.js:21)
    in ErrorBoundary (at src/index.js:20)
    in App (at src/index.js:29)

static getDerivedStateFromErrorとの違いだが、公式のAPIリファレンスによれば、static getDerivedStateFromErrorはUIの描画のために使い、副作用を扱う場合にcomponentDidCatchを使うとよいらしい。

キャッチできないエラー

既に述べたように Error Boundary は自身のエラーをキャッチすることが出来ないが、配下のコンポーネントのエラーでもキャッチしないものが2つある。

1つ目は、イベントハンドラ内でのエラー。
以下のようにボタンを押した際にエラーが発生するようにした場合、レンダリング時にはエラーを出さないので、そのまま表示される。

   <div style={{backgroundColor: 'aqua', padding: '10px'}}>
     aqua
     <AquaChild />
+    <button
+      type="button"
+      onClick={() => {
+        x;
+      }}
+    >
+      error button
+    </button>
   </div>

f:id:numb_86:20190615222013p:plain

そしてボタンを押すとエラーが発生するのだが、このエラーはキャッチされず、表示にも影響がない。

2つ目が、非同期処理のなかでのエラー。これも、エラーはどこにもキャッチされないまま終わり、アプリはそのまま表示され続ける。

const Aqua = () => {
  Promise.resolve().then(() => x);
  return (
    <div style={{backgroundColor: 'aqua', padding: '10px'}}>
      aqua
      <AquaChild />
    </div>
  );
};

参考資料