フロントエンドのエラー検知、後回しになっていませんか?
バックエンドでは、エラー検知や監視の仕組みを整えるのがもはや前提になっています。APIの障害やシステムの異常にすばやく対応できるよう、モニタリングツールや通知設定をしっかり導入しているチームも多いでしょう。
一方で、フロントエンドについてはどうでしょうか。
テストコードの文化は浸透してきたものの、本番環境でのエラー監視については、まだ手が回っていないケースも少なくありません。私自身も、フロント側のエラー検知の対応にはなかなか目が向いていませんでした。
テストだけではカバーしきれない本番特有のエラー。ネットワーク状況やブラウザ環境、ユーザーの操作状況など、実運用の中では予期しづらい問題が発生することもあります。
そこで重要になるのが、「実際にユーザーが体験している問題を検知できる仕組み」、つまりフロントエンドのエラー検知です。
今回は、そうしたフロントエンドエラーの検知をどう始めるか、そして無理なく導入するための方法を紹介します。
フロントエンドのエラー検知が後回しにされる理由
- ユーザーからのフィードバックがメインのエラー検出手段になっているケースも多く、フィードバックがない限り問題に気づけないことがある
- 端末・ブラウザ・通信環境などの多様性により、再現が困難
- SaaS導入の初期コスト(設定・SDK・通知設定など)がかかる
- 何もしないとノイズ(古いブラウザの警告、CORSエラーなど)が多すぎる
他の開発タスクとの兼ね合いで、後回しにされやすく導入のタイミングを見失いやすい領域でもあります。
検知すべきフロントエンドのエラー例
| 種別 | 内容 |
|---|---|
| JavaScriptエラー | window.onerror, unhandledrejection でキャッチ可能 |
| API通信エラー | CORS失敗、認証エラー(401/403)など |
| リソース読み込みエラー | 画像・CSS・スクリプトの読み込み失敗 |
なぜフロントエンドのエラー検知が必要なのか?
ユーザーが利用する環境は非常に多様です。どれほど丁寧にテストしても、全ての組み合わせをカバーするのは不可能です。
そのため、本番環境での実際のユーザーのエラーを可視化することが非常に重要になります。
よく使われるエラー検知サービス
| ツール名 | 特徴 |
|---|---|
| Sentry | OSS 由来で導入実績が多く、初学者にも比較的扱いやすい。無料枠もあるため小規模サービスでも使いやすい。 |
| Bugsnag | UI やアラート機能が分かりやすく、マネージャーや非エンジニアもログを確認しやすい設計になっている。 |
| Datadog RUM | 大規模サービスでの活用に強く、バックエンドとの統合にも向いている反面、コスト面は注意が必要。 |
| CloudWatch RUM | AWS にインフラを統一している場合に導入コストが低く、IAMやログ設計と連携しやすい。 |
クラウドサービス導入の課題
受託開発や他社サービスの開発では、以下のような導入障壁があります。
- ユーザー情報を外部に出せない
- 契約・合意が必要
- 外部サービス利用のコストがネックになることがある
このようなケースでは、内製でエラー検知を仕組み化する方法もあります。
ただし内製する場合にはいくつか注意点があります。
内製化の判断を下す前に知っておくべきこと
内製でのエラー検知は、特定の要件や小規模プロジェクトでは有効な選択肢となり得ます。しかし、本番環境での継続的な運用や、サービスの規模拡大を考慮すると、外部サービスにはない明確なリスクと限界が伴います。
JavaScriptコードがバンドル・ミニファイされている場合はエラーのスタックトレースを見ても、元のファイル名、行数、列数が不明なため、エラー箇所を特定するのが極めて困難です。
また、エラーが大量に発生した場合、すべてのエラーをサーバーに送信すると、ネットワーク負荷やサーバーのログ処理負荷が過大になります。かといって、単に制限すると重要なエラーを見逃す可能性があります。
単にエラーを検知するだけでなく、それを運用可能なレベルに引き上げ、セキュリティとプライバシーを保護するためには、これらの高度な課題に真剣に取り組む覚悟が必要です。そうでなければ、Sentryのような外部サービスを導入する方が、結果的にコスト効率と品質が高くなるでしょう。
内製でフロントエンドエラーを収集する方法
ここでは、APIサーバーにエラーを送信するシンプルな方法をご紹介します。
これらのコードはあくまで基本的な骨格であり、本番運用には上記の様々なリスクを考慮した追加実装や、運用体制の確立が不可欠です。
同期エラー、スクリプト読み込みエラーの検知
window.onerror = function (message, source, lineno, colno, error) {
reportError({
type: "onerror",
message,
source,
lineno,
colno,
stack: error?.stack,
});
};
Promise の unhandled rejection(非同期エラー)の検知
window.addEventListener('unhandledrejection', (event) => {
reportError({
type: "unhandledrejection",
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
});
});
API 通信など try/catch による明示的なエラーの検知
try {
const res = await fetch("/api/data");
if (!res.ok) throw new Error(`Status: ${res.status}`);
const data = await res.json();
} catch (err) {
reportError({
type: "api",
message: err.message,
stack: err.stack,
});
}
エラーレポート関数(共通)
function reportError(info: {
type: string;
message: string;
stack?: string;
[key: string]: any;
}) {
const payload = {
...info,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString(),
// 必要ならログインユーザーやクリック履歴も追加
};
fetch("/log-client-error", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
}).catch(console.error); // 二次エラー対策
}
フレームワークごとの対応
React
UI コンポーネントのレンダリング中に発生する例外を補足するために ErrorBoundary を使用する。
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error: any) {
return { hasError: true };
}
componentDidCatch(error: any, info: any) {
reportError({
type: "react-error",
message: error.message,
stack: error.stack,
});
}
render() {
if (this.state.hasError) return <h1>Something went wrong.</h1>;
return this.props.children;
}
}
// 使用法
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>;
※イベントハンドラ内の例外は try/catch を併用してください。
Vue
グローバルもしくはコンポーネント単位でエラーハンドラを設定する。
// グローバルハンドラ
app.config.errorHandler = (err, instance, info) => {
reportError({
type: "vue-global",
message: err.message,
stack: err.stack,
info,
});
};
// コンポーネント単位
defineComponent({
errorCaptured(err, instance, info) {
reportError({
type: "vue-component",
message: err.message,
stack: err.stack,
info,
});
return false;
},
});
ネットワークの負荷を抑えたい場合
reportError()関数でサーバー側にエラー情報を送信しているため、エラーが頻発する場合はネットワークの負荷が高まる恐れがあります。
その場合は一定時間ごとや一定量を超えた場合にまとめて送信するなどの方法を検討してください。
const errorBuffer: any[] = [];
// エラーをバッファリング
function bufferError(info: any) {
errorBuffer.push({
...info,
timestamp: new Date().toISOString(),
});
}
// 定期的に送信
setInterval(() => {
if (errorBuffer.length > 0) {
fetch("/log-client-error", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(errorBuffer),
})
.then(() => (errorBuffer.length = 0)) // 成功したらクリア
.catch(console.error);
}
}, 5000); // 5秒ごとに送信
エラーを送信した「その後」は?
本記事では、フロントエンドで発生したエラーをAPI経由で送信するシンプルな方法を紹介しました。ただし、送信したエラー情報をどう扱うか(保存・可視化・通知など)は、システム構成や目的に応じて設計が必要です。
例えば以下のような対応が考えられます。
- 送信されたログをデータベースに保存して後で分析する
- SlackやTeamsなどのチャットツールに通知し、迅速な対応を促す
- CloudWatch Logsなどでログ検索やフィルタリングを可能にする
まとめ
フロントエンドのエラーは、ユーザー体験に直結します。しかしその特性上、再現の難しさや多様な環境の影響により、把握が難しいこともあります。
だからこそ、本番環境でのエラーを適切に検知し、記録し、改善につなげる仕組みが重要です。
専用のモニタリングサービスを活用するもよし、プロジェクトに合わせて軽量な仕組みを内製するもよし。やり方はいろいろありますが、「ユーザーの見えないところで起きている問題に目を向ける」ことが、プロダクトの質を高める第一歩です。
チームやプロジェクトのフェーズに合わせて、できるところから少しずつ取り入れていけると良いですね。