JavaScript 非同期エラー完全攻略:Unhandled Rejection を根絶する設計パターン
JavaScript の非同期処理でアプリケーションが突然クラッシュしたり、エラーが握りつぶされたりした経験はありませんか?その原因の多くは Unhandled Promise Rejection です。本記事では、Unhandled Rejection の仕組みを理解し、設計レベルで根絶するための実践的なパターンを解説します。
初心者の方でも理解できるよう、基礎から応用まで段階的に説明していきますので、安心してお読みいただけます。この記事を読み終える頃には、堅牢な非同期エラーハンドリングを実装できるようになっているでしょう。
背景
JavaScript における非同期処理の進化
JavaScript は Single Thread で動作する言語ですが、非同期処理により効率的な I/O 処理を実現しています。非同期処理の仕組みは時代とともに進化してきました。
以下の図は、JavaScript の非同期処理がどのように進化してきたかを示しています。
mermaidflowchart LR
callback["Callback 関数<br/>(ES5)"] -->|コールバック地獄| promise["Promise<br/>(ES6)"]
promise -->|可読性向上| async["async/await<br/>(ES2017)"]
style callback fill:#ffcccc
style promise fill:#ffffcc
style async fill:#ccffcc
この図から、Callback から Promise、そして async/await へと移行することで、コードの可読性と保守性が大きく向上したことがわかります。
Promise が解決した課題
Promise の登場により、以下の課題が解決されました。
| # | 課題 | Promise による解決 |
|---|---|---|
| 1 | コールバック地獄(Callback Hell) | チェーン可能な .then() による平坦化 |
| 2 | エラーハンドリングの複雑さ | .catch() による統一的なエラー処理 |
| 3 | 非同期処理の状態管理 | Pending/Fulfilled/Rejected の 3 状態 |
| 4 | 複数の非同期処理の制御 | Promise.all() や Promise.race() |
しかし、Promise の普及に伴い、新たな問題が浮上しました。それが Unhandled Promise Rejection です。
Unhandled Promise Rejection とは
Unhandled Promise Rejection は、Promise が reject されたにもかかわらず、.catch() や try-catch でエラーをキャッチしていない状態を指します。
以下は、Unhandled Rejection が発生する基本的なフローです。
mermaidsequenceDiagram
participant Code as コード
participant Promise as Promise
participant EventLoop as Event Loop
participant Handler as Error Handler
Code->>Promise: 非同期処理実行
Promise->>Promise: エラー発生(reject)
Promise->>EventLoop: Rejection イベント
alt .catch() がある場合
EventLoop->>Handler: エラーハンドリング実行
Handler-->>Code: エラー処理完了
else .catch() がない場合
EventLoop->>Handler: unhandledRejection イベント発火
Handler-->>Code: プロセスクラッシュ(Node.js)<br/>コンソールエラー(Browser)
end
この図が示すように、エラーハンドリングが適切に行われない場合、アプリケーションの安定性に深刻な影響を及ぼします。
課題
Unhandled Rejection が引き起こす問題
Unhandled Promise Rejection は、以下のような深刻な問題を引き起こします。
サーバーサイドでの影響(Node.js)
Node.js 環境では、Unhandled Rejection は特に危険です。Node.js v15 以降、デフォルトで プロセスが終了する 動作に変更されました。
javascript// Unhandled Rejection の例
async function fetchUserData(userId) {
// エラーハンドリングなし
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
// この呼び出しでエラーが発生するとプロセスがクラッシュ
fetchUserData('invalid-id');
上記のコードでは、fetch が失敗した場合に .catch() や try-catch がないため、Unhandled Rejection が発生します。
実際のエラーメッセージは以下のようになります。
vbnetUnhandledPromiseRejectionWarning: Error: Request failed with status code 404
at fetchUserData (/app/index.js:3:15)
(Use `node --trace-warnings ...` to show where the warning was created)
UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch().
クライアントサイドでの影響(Browser)
ブラウザ環境では、プロセスクラッシュは起きませんが、以下の問題が発生します。
| # | 問題 | 影響 |
|---|---|---|
| 1 | エラーが握りつぶされる | ユーザーに適切なエラーメッセージが表示されない |
| 2 | 状態の不整合 | UI の状態が中途半端になる |
| 3 | デバッグの困難さ | エラーの原因特定に時間がかかる |
| 4 | ユーザー体験の悪化 | 画面がフリーズしたように見える |
よくある Unhandled Rejection のパターン
以下は、実際の開発現場で頻繁に見られる Unhandled Rejection のパターンです。
パターン 1:async 関数の呼び出し忘れ
async 関数を呼び出す際、await を付け忘れたり、エラーハンドリングを省略したりするケースです。
javascript// NG: エラーハンドリングなし
async function updateProfile(userId, data) {
const user = await getUserById(userId);
user.profile = data;
await saveUser(user); // このエラーが握りつぶされる
}
// この呼び出しでエラーが発生しても気づけない
updateProfile(123, { name: 'John' });
パターン 2:イベントハンドラー内での非同期処理
イベントハンドラー内で非同期処理を行う際、エラーハンドリングを忘れがちです。
javascript// NG: イベントハンドラー内でのエラーハンドリング欠如
button.addEventListener('click', async () => {
const data = await fetchData(); // エラーハンドリングなし
updateUI(data);
});
上記のコードでは、fetchData() がエラーを投げた場合、それが catch されません。
パターン 3:Promise.all での部分的なエラー
複数の Promise を並列実行する際、一部のエラーだけがキャッチされないケースです。
javascript// NG: 一部の Promise だけエラーハンドリング
const results = await Promise.all([
fetchUserData(1).catch((err) => null),
fetchUserData(2), // この Promise のエラーが Unhandled
fetchUserData(3).catch((err) => null),
]);
以下の図は、Unhandled Rejection が発生する典型的なコードパスを示しています。
mermaidflowchart TD
start["非同期関数呼び出し"] --> hasAwait{"await を<br/>使用?"}
hasAwait -->|Yes| hasTryCatch{"try-catch<br/>ブロック?"}
hasAwait -->|No| unhandled1["Unhandled Rejection<br/>発生"]
hasTryCatch -->|Yes| safe["安全"]
hasTryCatch -->|No| unhandled2["Unhandled Rejection<br/>発生"]
start --> chainPromise{"Promise<br/>チェーン?"}
chainPromise -->|Yes| hasCatch{".catch()<br/>メソッド?"}
chainPromise -->|No| hasEventHandler{"イベント<br/>ハンドラー?"}
hasCatch -->|Yes| safe
hasCatch -->|No| unhandled3["Unhandled Rejection<br/>発生"]
hasEventHandler -->|Yes| hasErrorHandler{"エラー<br/>ハンドラー?"}
hasEventHandler -->|No| end_task["処理終了"]
hasErrorHandler -->|Yes| safe
hasErrorHandler -->|No| unhandled4["Unhandled Rejection<br/>発生"]
style unhandled1 fill:#ffcccc
style unhandled2 fill:#ffcccc
style unhandled3 fill:#ffcccc
style unhandled4 fill:#ffcccc
style safe fill:#ccffcc
この図から、エラーハンドリングを行うべきポイントが複数あることがわかります。すべてのポイントで適切な対策を行う必要があります。
解決策
基本的なエラーハンドリングパターン
Unhandled Rejection を防ぐための基本パターンを、段階的に解説します。
パターン A:try-catch による同期的なエラーハンドリング
async/await を使用する場合、try-catch ブロックで囲むのが最も直感的です。
javascript// OK: try-catch でエラーをキャッチ
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('ユーザーデータの取得に失敗:', error);
throw error; // 上位でハンドリングする場合は再スロー
}
}
このコードでは、fetch の失敗と HTTP エラーの両方を適切にキャッチしています。エラーメッセージも具体的で、デバッグしやすくなっています。
try-catch を使用する際の注意点を表にまとめました。
| # | ポイント | 詳細 |
|---|---|---|
| 1 | エラーの再スロー | 上位でハンドリングする必要がある場合は throw error |
| 2 | エラーメッセージの具体化 | HTTP Error: 404 のように詳細情報を含める |
| 3 | ロギング | エラーを必ずログに記録する |
| 4 | リソースのクリーンアップ | finally ブロックでリソースを解放 |
パターン B:.catch() による Promise チェーン
Promise チェーンを使用する場合は、.catch() メソッドでエラーをキャッチします。
javascript// OK: .catch() でエラーハンドリング
function loadUserProfile(userId) {
return fetch(`/api/users/${userId}`)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.catch((error) => {
console.error('プロフィール読み込みエラー:', error);
// デフォルト値を返す
return { id: userId, name: 'Unknown' };
});
}
.catch() を使用すると、エラー時のフォールバック値を簡潔に返せます。上記では、エラー発生時にデフォルトのユーザーオブジェクトを返しています。
パターン C:Promise.allSettled() による並列処理
複数の Promise を並列実行する際、Promise.allSettled() を使用すると、一部が失敗してもすべての結果を取得できます。
javascript// OK: Promise.allSettled() で全結果を取得
async function fetchMultipleUsers(userIds) {
const promises = userIds.map((id) => fetchUserData(id));
const results = await Promise.allSettled(promises);
return results;
}
Promise.allSettled() は各 Promise の結果を以下の形式で返します。
javascript// 成功した Promise
{ status: 'fulfilled', value: { id: 1, name: 'John' } }
// 失敗した Promise
{ status: 'rejected', reason: Error('Not found') }
結果の処理は以下のように行います。
javascriptconst results = await fetchMultipleUsers([1, 2, 3]);
// 成功したユーザーだけを抽出
const successUsers = results
.filter((result) => result.status === 'fulfilled')
.map((result) => result.value);
// 失敗したユーザーIDをログ出力
results
.filter((result) => result.status === 'rejected')
.forEach((result, index) => {
console.error(
`User ${index + 1} 取得失敗:`,
result.reason
);
});
設計パターンによる根絶策
基本的なエラーハンドリングに加えて、設計レベルで Unhandled Rejection を防ぐパターンを紹介します。
設計パターン 1:Error Boundary パターン
アプリケーション全体でエラーをキャッチする境界を設けるパターンです。
以下の図は、Error Boundary パターンの構造を示しています。
mermaidflowchart TD
app["Application"] --> boundary["Error Boundary<br/>(最上位)"]
boundary --> module1["Module A<br/>Error Handler"]
boundary --> module2["Module B<br/>Error Handler"]
boundary --> module3["Module C<br/>Error Handler"]
module1 --> func1["async 関数群"]
module2 --> func2["async 関数群"]
module3 --> func3["async 関数群"]
func1 -.->|エラー| module1
func2 -.->|エラー| module2
func3 -.->|エラー| module3
module1 -.->|未処理エラー| boundary
module2 -.->|未処理エラー| boundary
module3 -.->|未処理エラー| boundary
style boundary fill:#ffcccc
style module1 fill:#ffffcc
style module2 fill:#ffffcc
style module3 fill:#ffffcc
図が示すように、各モジュールでキャッチされなかったエラーは、最上位の Error Boundary でキャッチします。
実装例を見てみましょう。
javascript// Error Boundary の実装
class AsyncErrorBoundary {
constructor() {
this.errorHandlers = [];
this.setupGlobalHandlers();
}
setupGlobalHandlers() {
// Node.js 環境
if (typeof process !== 'undefined') {
process.on('unhandledRejection', (reason, promise) => {
this.handleError(reason, { promise });
});
}
Global ハンドラーの設定により、すべての Unhandled Rejection をキャッチできます。
javascript // Browser 環境
if (typeof window !== 'undefined') {
window.addEventListener('unhandledrejection', (event) => {
this.handleError(event.reason, { event });
event.preventDefault(); // デフォルト動作を抑制
});
}
}
ブラウザでは unhandledrejection イベントをリスニングします。preventDefault() を呼ぶことで、コンソールエラーの出力を制御できます。
javascript handleError(error, context = {}) {
// エラー情報を整形
const errorInfo = {
message: error.message || String(error),
stack: error.stack,
timestamp: new Date().toISOString(),
context
};
// 登録されたハンドラーを実行
this.errorHandlers.forEach(handler => {
try {
handler(errorInfo);
} catch (handlerError) {
console.error('Error handler 自体がエラー:', handlerError);
}
});
}
エラーハンドラー自体がエラーを投げる可能性も考慮して、try-catch で保護しています。
javascript registerHandler(handler) {
this.errorHandlers.push(handler);
}
removeHandler(handler) {
const index = this.errorHandlers.indexOf(handler);
if (index > -1) {
this.errorHandlers.splice(index, 1);
}
}
}
Error Boundary の使用例は以下の通りです。
javascript// Error Boundary の使用
const errorBoundary = new AsyncErrorBoundary();
// カスタムエラーハンドラーの登録
errorBoundary.registerHandler((errorInfo) => {
console.error('Unhandled エラーを検知:', errorInfo);
// ログサービスに送信
logToServer(errorInfo);
// ユーザーに通知
showErrorNotification('エラーが発生しました');
});
設計パターン 2:Safe Async Wrapper パターン
すべての非同期関数を自動的にエラーハンドリングでラップするパターンです。
javascript// Safe Async Wrapper の実装
function safeAsync(asyncFn, options = {}) {
const {
onError = (error) => console.error(error),
defaultValue = null,
retries = 0,
retryDelay = 1000
} = options;
return async function(...args) {
let lastError;
// リトライロジック
for (let attempt = 0; attempt <= retries; attempt++) {
Wrapper 関数では、リトライ機能も実装しています。リトライ回数と遅延時間はオプションで指定できます。
javascript try {
const result = await asyncFn.apply(this, args);
return result;
} catch (error) {
lastError = error;
// 最後のリトライでない場合は待機
if (attempt < retries) {
await new Promise(resolve =>
setTimeout(resolve, retryDelay)
);
continue;
}
リトライの間に遅延を設けることで、一時的なネットワークエラーなどから回復できます。
javascript // すべてのリトライが失敗
onError(error);
return defaultValue;
}
}
};
}
使用例を見てみましょう。
javascript// Safe Async Wrapper の使用例
const safeFetchUser = safeAsync(
async (userId) => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok)
throw new Error(`HTTP ${response.status}`);
return response.json();
},
{
onError: (err) =>
console.error('ユーザー取得エラー:', err),
defaultValue: { id: null, name: 'Unknown' },
retries: 2,
retryDelay: 1000,
}
);
// エラーハンドリングを意識せずに使用可能
const user = await safeFetchUser(123);
console.log(user.name); // エラー時も安全にアクセスできる
この Wrapper を使えば、個別の try-catch を書かずに済みます。
設計パターン 3:Result Type パターン
エラーを値として扱い、型安全なエラーハンドリングを実現するパターンです。このパターンは Rust や Go などの言語で採用されています。
typescript// Result 型の定義
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
// Result を返す関数
async function fetchUserResult(userId: number): Promise<Result<User>> {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
return {
ok: false,
error: new Error(`HTTP ${response.status}`)
};
}
エラーが発生した場合、例外をスローするのではなく、ok: false のオブジェクトを返します。
typescript const user = await response.json();
return { ok: true, value: user };
} catch (error) {
return {
ok: false,
error: error instanceof Error ? error : new Error(String(error))
};
}
}
Result Type の使用例は以下の通りです。
typescript// Result 型の使用
const result = await fetchUserResult(123);
if (result.ok) {
// TypeScript が value の型を正しく推論
console.log(result.value.name);
} else {
// TypeScript が error の型を正しく推論
console.error('エラー:', result.error.message);
}
このパターンの利点は、エラーハンドリングを強制できる点です。result.ok をチェックしないと、TypeScript がコンパイルエラーを出します。
具体例
実践例 1:API クライアントの実装
実際のプロジェクトで使える、堅牢な API クライアントを実装してみましょう。
API クライアントの基本構造
まず、API クライアントの全体構造を図で確認します。
mermaidflowchart LR
app["Application"] -->|リクエスト| client["API Client"]
client -->|前処理| interceptor["Request Interceptor"]
interceptor -->|HTTP| api["Backend API"]
api -->|レスポンス| handler["Response Handler"]
handler -->|エラー判定| errorHandler["Error Handler"]
errorHandler -->|Result| app
errorHandler -.->|リトライ| interceptor
errorHandler -.->|ログ| logger["Logger"]
style errorHandler fill:#ffcccc
style client fill:#ccffcc
この図から、リクエストからレスポンスまでの流れと、エラーハンドリングの位置づけが理解できます。
Step 1:基本クラスの定義
API クライアントの基本クラスを定義します。
typescript// API クライアントの基本クラス
class ApiClient {
private baseUrl: string;
private defaultHeaders: Record<string, string>;
private requestInterceptors: Array<RequestInterceptor> = [];
private responseInterceptors: Array<ResponseInterceptor> = [];
constructor(config: ApiClientConfig) {
this.baseUrl = config.baseUrl;
this.defaultHeaders = config.headers || {};
}
Step 2:リクエストメソッドの実装
HTTP リクエストを送信する基本メソッドを実装します。
typescript async request<T>(
endpoint: string,
options: RequestOptions = {}
): Promise<Result<T>> {
const url = `${this.baseUrl}${endpoint}`;
const config = this.buildRequestConfig(options);
try {
// Request Interceptor の実行
const modifiedConfig = await this.runRequestInterceptors(config);
Request Interceptor では、認証トークンの追加やリクエストのログ出力などを行います。
typescript // Fetch の実行
const response = await fetch(url, modifiedConfig);
// Response Interceptor の実行
const processedResponse = await this.runResponseInterceptors(response);
// レスポンスの検証
return await this.handleResponse<T>(processedResponse);
} catch (error) {
return this.handleError(error);
}
}
Step 3:レスポンスハンドリング
レスポンスを適切に処理するメソッドを実装します。
typescript private async handleResponse<T>(response: Response): Promise<Result<T>> {
// HTTP ステータスコードのチェック
if (!response.ok) {
const errorBody = await response.text();
return {
ok: false,
error: {
code: `HTTP_${response.status}`,
message: `HTTP Error: ${response.status} ${response.statusText}`,
details: errorBody,
statusCode: response.status
}
};
}
HTTP エラーの場合、詳細なエラー情報を含む Result オブジェクトを返します。
typescript // JSON のパース
try {
const data = await response.json();
return { ok: true, value: data as T };
} catch (parseError) {
return {
ok: false,
error: {
code: 'PARSE_ERROR',
message: 'レスポンスのパースに失敗しました',
details: parseError
}
};
}
}
Step 4:エラーハンドリング
包括的なエラーハンドリングを実装します。
typescript private handleError(error: unknown): Result<never> {
// ネットワークエラー
if (error instanceof TypeError && error.message.includes('fetch')) {
return {
ok: false,
error: {
code: 'NETWORK_ERROR',
message: 'ネットワークエラーが発生しました',
details: error,
retryable: true
}
};
}
ネットワークエラーの場合、retryable: true フラグを設定し、リトライ可能であることを示します。
typescript// タイムアウトエラー
if (error instanceof Error && error.name === 'AbortError') {
return {
ok: false,
error: {
code: 'TIMEOUT_ERROR',
message: 'リクエストがタイムアウトしました',
details: error,
retryable: true,
},
};
}
typescript // その他のエラー
return {
ok: false,
error: {
code: 'UNKNOWN_ERROR',
message: '予期しないエラーが発生しました',
details: error
}
};
}
Step 5:リトライロジックの実装
自動リトライ機能を追加します。
typescript async requestWithRetry<T>(
endpoint: string,
options: RequestOptions = {},
retryConfig: RetryConfig = { maxRetries: 3, delay: 1000 }
): Promise<Result<T>> {
let lastResult: Result<T>;
for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
lastResult = await this.request<T>(endpoint, options);
typescript // 成功した場合はそのまま返す
if (lastResult.ok) {
return lastResult;
}
// リトライ不可能なエラーの場合は即座に返す
if (!lastResult.error.retryable) {
return lastResult;
}
// 最後のリトライでない場合は待機
if (attempt < retryConfig.maxRetries) {
await this.delay(retryConfig.delay * Math.pow(2, attempt));
}
}
return lastResult!;
}
Exponential Backoff を実装しており、リトライごとに待機時間が増加します。
API クライアントの使用例
実装した API クライアントを使用してみましょう。
typescript// API クライアントの初期化
const apiClient = new ApiClient({
baseUrl: 'https://api.example.com',
headers: {
'Content-Type': 'application/json',
},
});
// ユーザー情報の取得
const result = await apiClient.requestWithRetry<User>(
'/users/123',
{ method: 'GET' }
);
if (result.ok) {
console.log('ユーザー名:', result.value.name);
} else {
// エラーコードに応じた処理
switch (result.error.code) {
case 'HTTP_404':
console.error('ユーザーが見つかりません');
break;
case 'NETWORK_ERROR':
console.error('ネットワークに接続できません');
break;
default:
console.error('エラー:', result.error.message);
}
}
実践例 2:React での実装
React アプリケーションでの Unhandled Rejection 対策を実装します。
カスタムフックの作成
非同期処理を安全に扱うカスタムフックを作成しましょう。
typescript// useAsyncSafe フックの実装
function useAsyncSafe<T>(
asyncFunction: () => Promise<T>,
dependencies: React.DependencyList = []
) {
const [state, setState] = React.useState<{
data: T | null;
error: Error | null;
loading: boolean;
}>({
data: null,
error: null,
loading: true
});
State には、data、error、loading の 3 つのプロパティを持たせます。
typescript React.useEffect(() => {
let cancelled = false;
const execute = async () => {
try {
setState(prev => ({ ...prev, loading: true, error: null }));
const result = await asyncFunction();
// コンポーネントがアンマウントされていない場合のみ更新
if (!cancelled) {
setState({ data: result, error: null, loading: false });
}
cancelled フラグにより、アンマウント後の State 更新を防ぎます。これは React の Warning を防ぐ重要なパターンです。
typescript } catch (error) {
if (!cancelled) {
setState({
data: null,
error: error instanceof Error ? error : new Error(String(error)),
loading: false
});
}
}
};
execute();
// クリーンアップ関数
return () => {
cancelled = true;
};
}, dependencies);
return state;
}
エラーバウンダリーコンポーネント
React の Error Boundary を拡張して、非同期エラーもキャッチできるようにします。
typescript// AsyncErrorBoundary コンポーネント
class AsyncErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback: React.ComponentType<{ error: Error }> },
{ error: Error | null }
> {
constructor(props) {
super(props);
this.state = { error: null };
}
static getDerivedStateFromError(error: Error) {
return { error };
}
typescript componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// エラーログの送信
console.error('React Error Boundary がエラーをキャッチ:', error, errorInfo);
}
componentDidMount() {
// Unhandled Rejection のリスナーを追加
window.addEventListener('unhandledrejection', this.handleUnhandledRejection);
}
componentWillUnmount() {
window.removeEventListener('unhandledrejection', this.handleUnhandledRejection);
}
typescript handleUnhandledRejection = (event: PromiseRejectionEvent) => {
event.preventDefault();
this.setState({
error: event.reason instanceof Error
? event.reason
: new Error(String(event.reason))
});
};
render() {
if (this.state.error) {
const FallbackComponent = this.props.fallback;
return <FallbackComponent error={this.state.error} />;
}
return this.props.children;
}
}
使用例
作成したフックとコンポーネントを組み合わせて使用します。
typescript// エラー表示コンポーネント
function ErrorFallback({ error }: { error: Error }) {
return (
<div style={{ padding: '20px', color: 'red' }}>
<h2>エラーが発生しました</h2>
<p>{error.message}</p>
<button onClick={() => window.location.reload()}>
ページを再読み込み
</button>
</div>
);
}
// メインコンポーネント
function UserProfile({ userId }: { userId: number }) {
const { data, error, loading } =
useAsyncSafe(async () => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok)
throw new Error(`HTTP ${response.status}`);
return response.json();
}, [userId]);
if (loading) return <div>読み込み中...</div>;
if (error) throw error; // Error Boundary でキャッチ
return <div>ようこそ、{data.name}さん</div>;
}
typescript// アプリケーションのルート
function App() {
return (
<AsyncErrorBoundary fallback={ErrorFallback}>
<UserProfile userId={123} />
</AsyncErrorBoundary>
);
}
実践例 3:Node.js サーバーでの実装
Express を使った Node.js サーバーでの包括的なエラーハンドリングを実装します。
グローバルエラーハンドラーの設定
サーバー起動時にグローバルなエラーハンドラーを設定します。
javascript// サーバーのエントリーポイント
import express from 'express';
const app = express();
// Unhandled Rejection のハンドラー
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// ログサービスに送信
logger.error('Unhandled Rejection', {
reason: reason,
stack: reason instanceof Error ? reason.stack : undefined
});
javascript // 開発環境では詳細を表示、本番環境では graceful shutdown
if (process.env.NODE_ENV === 'production') {
// graceful shutdown の実行
gracefulShutdown();
}
});
// Uncaught Exception のハンドラー
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
logger.error('Uncaught Exception', {
error: error.message,
stack: error.stack
});
// 即座にシャットダウン
process.exit(1);
});
非同期ルートハンドラーのラッパー
Express のルートハンドラーで発生したエラーを自動的にキャッチする Wrapper を作成します。
javascript// asyncHandler ラッパー
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next); // エラーを次のミドルウェアに渡す
};
}
// 使用例
app.get(
'/api/users/:id',
asyncHandler(async (req, res) => {
const userId = req.params.id;
// エラーが発生しても asyncHandler が自動的にキャッチ
const user = await getUserById(userId);
res.json(user);
})
);
エラーハンドリングミドルウェア
すべてのエラーを処理する中央集権的なミドルウェアを実装します。
javascript// エラーハンドリングミドルウェア
app.use((error, req, res, next) => {
// エラーのログ出力
console.error('Error occurred:', {
message: error.message,
stack: error.stack,
url: req.url,
method: req.method,
body: req.body
});
javascript// エラーの種類に応じたレスポンス
if (error.name === 'ValidationError') {
return res.status(400).json({
error: {
code: 'VALIDATION_ERROR',
message: 'バリデーションエラーが発生しました',
details: error.details,
},
});
}
javascript if (error.name === 'UnauthorizedError') {
return res.status(401).json({
error: {
code: 'UNAUTHORIZED',
message: '認証が必要です'
}
});
}
// デフォルトのエラーレスポンス
res.status(500).json({
error: {
code: 'INTERNAL_SERVER_ERROR',
message: process.env.NODE_ENV === 'production'
? 'サーバーエラーが発生しました'
: error.message
}
});
});
本番環境では詳細なエラーメッセージを隠し、開発環境では表示することでセキュリティとデバッグの両立を図ります。
Graceful Shutdown の実装
サーバーを安全にシャットダウンする処理を実装します。
javascript// Graceful Shutdown の実装
function gracefulShutdown() {
console.log('Graceful shutdown を開始します...');
server.close(() => {
console.log('HTTP サーバーをクローズしました');
// データベース接続のクローズ
closeDatabase()
.then(() => {
console.log('データベース接続をクローズしました');
process.exit(0);
})
.catch(err => {
console.error('データベースクローズエラー:', err);
process.exit(1);
});
});
javascript // タイムアウト設定(30秒)
setTimeout(() => {
console.error('Graceful shutdown がタイムアウトしました');
process.exit(1);
}, 30000);
}
// SIGTERM シグナルの処理
process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);
まとめ
本記事では、JavaScript の Unhandled Promise Rejection を根絶するための包括的な方法を解説しました。重要なポイントを振り返りましょう。
Unhandled Rejection を防ぐための基本原則
| # | 原則 | 実践方法 |
|---|---|---|
| 1 | すべての Promise にエラーハンドリングを付ける | try-catch または .catch() を必ず使用 |
| 2 | async 関数の呼び出しに注意する | await を付け、適切にエラーをキャッチ |
| 3 | イベントハンドラー内の非同期処理を保護する | Wrapper 関数でエラーを自動キャッチ |
| 4 | グローバルハンドラーを設定する | 最後の砦としての Error Boundary |
推奨する設計パターン
本記事で紹介した 3 つの設計パターンは、それぞれ異なる状況で効果を発揮します。
Error Boundary パターン は、アプリケーション全体の安全網として機能します。すべての Unhandled Rejection を最終的にキャッチし、ログ出力やユーザー通知を一元管理できます。
Safe Async Wrapper パターン は、個々の非同期関数を保護し、リトライやデフォルト値の返却を自動化します。API 呼び出しなど、失敗する可能性のある処理に最適です。
Result Type パターン は、型安全性を重視する TypeScript プロジェクトで威力を発揮します。エラーを値として扱うことで、コンパイル時にエラーハンドリングの漏れを検出できます。
環境別の対策まとめ
ブラウザ環境では、window.addEventListener('unhandledrejection') でグローバルハンドラーを設定し、React の場合は Error Boundary コンポーネントと組み合わせましょう。
Node.js 環境では、process.on('unhandledRejection') と process.on('uncaughtException') の両方を設定し、Graceful Shutdown を実装することが重要です。本番環境では特に、予期しないエラーでサーバーが停止しないよう、適切な対策が必要になります。
デバッグとモニタリング
Unhandled Rejection を防ぐだけでなく、発生したエラーを適切にログ出力し、監視することも重要です。エラーメッセージには以下の情報を含めましょう。
- エラーコード(HTTP ステータス、カスタムエラーコードなど)
- エラーメッセージ(具体的で検索可能な内容)
- スタックトレース
- 発生時刻とコンテキスト情報
これらの情報があれば、本番環境で発生したエラーの原因を迅速に特定できます。
次のステップ
本記事で学んだパターンを実際のプロジェクトに適用してみてください。最初は小さな機能から始め、徐々に範囲を広げていくと良いでしょう。
特に重要なのは、チーム全体でエラーハンドリングの方針を統一することです。コーディング規約に本記事で紹介したパターンを組み込み、レビュー時にエラーハンドリングの漏れをチェックする習慣をつけましょう。
Unhandled Rejection のない、堅牢なアプリケーションを構築できるよう応援しています。
関連リンク
本記事で扱ったトピックについて、さらに深く学びたい方は以下の公式ドキュメントやリソースをご参照ください。
- MDN Web Docs - Promise - Promise の基本と使い方
- MDN Web Docs - async/await - async/await の詳細解説
- Node.js 公式ドキュメント - Error handling - Node.js でのエラーハンドリング
- React 公式ドキュメント - Error Boundaries - React のエラーバウンダリー
- TypeScript 公式ドキュメント - TypeScript での型安全なエラーハンドリング
- Express エラーハンドリング - Express でのエラー処理
- Promise/A+ 仕様 - Promise の標準仕様
articleJavaScript 非同期エラー完全攻略:Unhandled Rejection を根絶する設計パターン
article【早見表】JavaScript MutationObserver & ResizeObserver 徹底活用:DOM 変化を正しく監視する
articleJavaScript Drag & Drop API 完全攻略:ファイルアップロード UI を最速で作る
articleJavaScript structuredClone 徹底検証:JSON 方式や cloneDeep との速度・互換比較
articleJavaScript 時刻の落とし穴大全:タイムゾーン/DST/うるう秒の実務対策
articleJavaScript Web Animations API:滑らかに動く UI を設計するための基本と実践
articleApollo Client の正規化設計:`keyFields`/`typePolicies` で ID 設計を固定化
articleCursor コスト最適化:トークン節約・キャッシュ・差分駆動で費用を半減
articleZod の再帰型・木構造設計:`z.lazy` でツリー/グラフを安全に表現
articleCline ガバナンス運用:ポリシー・承認フロー・監査証跡の整備
articleYarn の歴史と進化:Classic(v1) から Berry(v2/v4) まで一気に把握
articleClaude Code 中心の開発プロセス設計:要求 → 設計 → 実装 → 検証の最短動線
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来