Next.js Error Boundary カスタマイズしたかった話
フロントエンド エラートラッキング
フロントエンドエンジニアのみなさん。お仕事で開発をしていると、運用・保守という宿命から逃れることはできません。
エラーとは日々戦っていますか? それとも見なかったことにしてますか?
Error Boundary
React では Error Boundary を使うことで、例外的なエラーをキャッチしてアプリがクラッシュするのを防ぐことができます。ただし、一部のエラーは Error Boundary では捕捉できません。たとえば、イベントハンドラ内のエラーなどは対象外です。
基本的な Error Boundary の実装
Error Boundary は Class コンポーネントで実装する必要があります。
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
react-error-boundary パッケージを使用すれば、関数コンポーネントでError Boundaryを使用することができますね。
import { ErrorBoundary } from "react-error-boundary";
export default function Search() {
return (
<ErrorBoundary
fallback={<p>There was an error while submitting the form</p>}
>
</ErrorBoundary>
);
}
いざ、エラートラッキングサービスを変更
ある日迷えるフロントエンドエンジニアは、エラートラッキングサービスを変更することになりました。
具体的なサービス名は割愛します。
今までの Error Boundary の実装は、先人のコードをそのまま利用しており、特に手を加えることなく過ごしていました。しかし、今回の変更に伴い、自前の Error Boundary を書き直すことになりました。
interface ErrorBoundaryState {
error: Error | null
}
class ErrorBoundary extends React.Component<{ children: ReactNode }, ErrorBoundaryState> {
constructor(props: { children: ReactNode }) {
super(props)
this.state = { error: null }
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
const renderingError = new Error(error.message)
renderingError.name = `ReactRenderingError`
if (info.componentStack) renderingError.stack = info.componentStack
renderingError.cause = error
reportError(renderingError)
this.setState({ error: renderingError })
}
render() {
if (this.state.error) {
return <ErrorPage error={this.state.error} info={{ componentStack: this.state.error.stack }} />
}
return this.props.children
}
}
export default ErrorBoundary
一般的な実装であり、Error Boundaryまで例外が捕捉されると、reportErrorが発火されます。
reportErrorを、SentryやDatadogなどのエラートラッキングサービスにログが送信される仕組みになっています。
事件
実装が完了し、テストをしてみると
なぜか、同じエラーが異なるエラークラスで発火している!?
具体的な挙動(エラートレース)
Error: test
at pages/home.js:1:11131
at chunks/framework.js:1:43341
at chunks/framework.js:1:136237
at chunks/framework.js:1:190698
Handled by:
Error:
at build/installHook.js:1:146768
at chunks/framework.js:1:60282
at chunks/framework.js:1:85654
at chunks/framework.js:1:91126
ReactRenderingError: test
at pages/home.js:1:11131
at <anonymous>
at pages/_app.js:1:9876
at chunks/main.js:1:13672
at chunks/main.js:1:16732
Caused by: Error: test
at pages/home.js:1:11131
at chunks/framework.js:1:43341
at chunks/framework.js:1:63225
at chunks/framework.js:1:136237
at chunks/framework.js:1:190698
Handled by:
Error:
at pages/_app.js:40:388328
at chunks/framework.js:1:61080
at chunks/framework.js:1:101148
at chunks/framework.js:1:90642
Next.js の背景処理が原因だった
onUncaughtError という関数が Next.js のコード内にあり、これが reportError を二重に発火させていました。
export const onUncaughtError: HydrationOptions['onUncaughtError'] = (
err,
errorInfo
) => {
if (isBailoutToCSRError(err) || isNextRouterError(err)) return;
if (process.env.NODE_ENV !== 'production') {
reportGlobalError(getReactStitchedError(err));
} else {
reportGlobalError(err);
}
}
Next.js の内部でも reportError が発火していました。
エラークラスをカスタマイズしたかったのに、Next.jsが勝手にエラーをラップしているせいで、私の意図とは違う形で、トラッキングされてしまいました。
これ、どうにか Error Boundary 内で回避できないんですかね。
以上愚痴でした
まとめ
エラートラッキングは、ただサービスを導入すれば終わりではなく、フレームワークの挙動も理解しないといけないという良い学びになりました。
Next.js のエラーハンドリングをどう回避するか(解決編)をお楽しみに!(未定)
Discussion