SPA
React
next.js
ssr
Next.jsDay 5

Next.jsのエラーハンドリング

Next.jsを実践投入するには、エラーハンドリングまわりも考えないといけません。関心事は、サイト独自のエラーページデザインであったり、Sentryなどでのエラー通知だと思います。
まずはNext.jsが用意しているエラーページを見ていきましょう。

本記事は公式ドキュメントのエラーハンドリングの項目を一部翻訳しています。
https://github.com/zeit/next.js#custom-error-handling

エラーハンドリングのカスタマイズ

Next.jsではデフォルトのエラーハンドリングページが用意されています。ステータスコードが404もしくは500のエラーをキャッチし、クライアントとサーバーサイドの両方でハンドリングされます。
もしデフォルトのエラーコンポーネントをオーバーライドしたいのなら、_error.jsというファイルをpagesフォルダーの中に定義しましょう:

pages/_error.js
import React from 'react'

export default class Error extends React.Component {
  static getInitialProps({ res, err }) {
    const statusCode = res ? res.statusCode : err ? err.statusCode : null;
    return { statusCode }
  }

  render() {
    return (
      <p>
        {`${this.props.statusCode}エラーが起こりました。`}
      </p>
    )
  }
}

エラーハンドリングは、独自の_errorコンポーネントをインポートして行います。

pages/index.js
import React from 'react'
import Error from './_error'
import fetch from 'isomorphic-unfetch'

export default class Page extends React.Component {
  static async getInitialProps() {
    const res = await fetch('https://api.github.com/repos/zeit/next.js')
    const statusCode = res.statusCode > 200 ? res.statusCode : false
    const json = await res.json()

    return { statusCode, stars: json.stargazers_count }
  }

  render() {
    if (this.props.statusCode) {
      return <Error statusCode={this.props.statusCode} />
    }

    return (
      <div>
        Next stars: {this.props.stars}
      </div>
    )
  }
}

ここで少し注意したいのは、_error.jsgetInitialProps()はサーバから直接レンダリングされた場合のみ機能します。例えば存在しないページhttp://localhost:3000/hogefugaにアクセスしたときはgetInitialProps()を通過し、statusCodeを取得して404ページがレンダリングされます。逆に、pages/index.jsの例のように、ページの処理途中にエラーが起こった場合は_error.jsは普通のReactコンポーネントと同じ扱いで呼び出されるため、getInitialProps()は実行されません。

また、Next.jsが用意するエラーページを再利用したい場合は'./_error'ではなく'next/error'から同様にインポートしましょう。

Sentryの導入

前日の_app.jsについての記事でも触れましたが、<App>コンポーネントは全てのページにおいて、初期化時に利用されます。
ですので、このコンポーネントのライフサイクルcomponentDidCatchでエラーハンドリングを行えば、全エラーの捕捉が可能です。

公式の例より

pages/_app.js
import App from 'next/app'
import * as Sentry from '@sentry/browser'

const SENTRY_PUBLIC_DSN = ''

export default class MyApp extends App {
  constructor (...args) {
    super(...args)
    Sentry.init({dsn: SENTRY_PUBLIC_DSN})
  }

  componentDidCatch (error, errorInfo) {
    Sentry.configureScope(scope => {
      Object.keys(errorInfo).forEach(key => {
        scope.setExtra(key, errorInfo[key])
      })
    })
    Sentry.captureException(error)

    // This is needed to render errors correctly in development / production
    super.componentDidCatch(error, errorInfo)
  }
}

しかし、上記例の場合だと実はサーバーサイドのエラーもクライアントと同じように拾ってしまいます。'@sentry/browser'はサーバーサイドでも一応動くようです。
一旦入れる場合にはよいかもしれませんが、サーバーとクライアントを区別してエラーハンドリングを行う方法を、まさにIssueで議論が行われています。注視していきたいです。
Improve with-sentry example #5727