TypeScriptで非同期処理を書いていると、「オブジェクトの一部のプロパティだけをPromiseにしたい」というパターンに遭遇することがあります。このパターンは一見シンプルに見えますが、実装を誤ると実行時にUnhandled Rejection
という問題を引き起こす可能性があります。
本稿では、このパターンを安全に実装する方法について解説します。
非同期処理の部分適用パターン
まず、シンプルなタスク処理の例で考えてみましょう:
// タスク結果の型
export type TaskResult = {
status: string;
value: number;
};
// 部分的にPromiseにした型
export type AsyncTask = {
status: Promise<string>;
value: Promise<number>;
result: Promise<TaskResult>;
};
よくある実装の問題点
このパターンを単純に実装すると、次のような実行時の問題が発生します:
import type { AsyncTask, TaskResult } from "./type";
// unhandledRejectionをキャッチして表示
process.on("unhandledRejection", (reason) => {
console.log("Unhandled Rejection:", (reason as Error).message);
});
// ❌ 問題のある実装
function runTask(): AsyncTask {
const resultPromise = new Promise<TaskResult>((resolve, reject) => {
setTimeout(() => {
reject(new Error("タスク失敗!"));
}, 1000);
});
return {
status: resultPromise.then(result => result.status),
value: resultPromise.then(result => result.value),
result: resultPromise
};
}
// 問題のある使用例
const task = runTask();
// try-catchしているはずなのに、Unhandled Rejectionが発生する↓
try {
const status = await task.status;
console.log(status);
} catch (error) {
console.error("try-catchでキャッチしたエラー:", (error as Error).message);
}
実行結果:
try-catchでキャッチしたエラー: タスク失敗!
Unhandled Rejection: タスク失敗!
この実装には、使用するプロパティをtry-catchで囲んでも、他のプロパティでもUnhandled Rejection
が発生する(上記の実行結果のように2つのエラーが表示される)という問題があります。
安全な実装パターン
これらの問題を解決する実装を見ていきましょう:
import type { AsyncTask, TaskResult } from "./type";
// unhandledRejectionをキャッチして表示
process.on("unhandledRejection", (reason) => {
console.log("Unhandled Rejection:", (reason as Error).message);
});
// ✅ 改善された実装
function runTask(): AsyncTask {
const resultPromise = new Promise<TaskResult>((resolve, reject) => {
setTimeout(() => {
reject(new Error("タスク失敗!"));
}, 1000);
});
// 重要: 元のPromiseに空のcatchハンドラーを追加
resultPromise.catch(() => {});
// getterを使用して返す
return {
get status() {
return resultPromise.then(result => result.status);
},
get value() {
return resultPromise.then(result => result.value);
},
get result() {
return resultPromise;
}
};
}
// 使用例
const task = runTask();
try {
const status = await task.status;
console.log(status);
} catch (error) {
console.error("try-catchでキャッチしたエラー:", (error as Error).message);
}
実行結果:
try-catchでキャッチしたエラー: タスク失敗!
改善された実装では、エラーは1回だけ表示され、使用していないプロパティでUnhandled Rejectionは発生しません。
安全な実装のポイント
この実装パターンには2つの重要な工夫があります:
1. 元のPromiseへの空のcatchハンドラーの追加
resultPromise.catch(() => {});
これにより、Unhandled Rejectionを防ぎます。
これだけだと不十分で、後述の「2. getterの使用」が必要です。もし、この工夫だけだと、使用していないプロパティでもエラーが発生します:
import type { AsyncTask, TaskResult } from "./type";
// unhandledRejectionをキャッチして表示
process.on("unhandledRejection", (reason) => {
console.log("Unhandled Rejection:", (reason as Error).message);
});
function runTask(): AsyncTask {
const resultPromise = new Promise<TaskResult>((resolve, reject) => {
setTimeout(() => {
reject(new Error("タスク失敗!"));
}, 1000);
});
// 重要: 元のPromiseに空のcatchハンドラーを追加
resultPromise.catch(() => {});
return {
status: resultPromise.then(result => result.status),
value: resultPromise.then(result => result.value),
result: resultPromise
};
}
// 問題のある使用例
const task = runTask();
try {
const status = await task.status;
console.log(status);
} catch (error) {
console.error("try-catchでキャッチしたエラー:", (error as Error).message);
}
実行結果:
try-catchでキャッチしたエラー: タスク失敗!
Unhandled Rejection: タスク失敗!
catchハンドラーだけでは、エラーが2回表示され、問題が解決していないことがわかります。
2. getterの使用
get status() { ... }
遅延評価を実現し、必要なときだけPromiseを評価します。
getterだけでも不十分です。以下のように、プロパティにアクセスしない場合、Unhandled Rejectionが発生してしまいます:
import type { AsyncTask, TaskResult } from "./type";
// unhandledRejectionをキャッチして表示
process.on("unhandledRejection", (reason) => {
console.log("Unhandled Rejection:", (reason as Error).message);
});
function runTask(): AsyncTask {
const resultPromise = new Promise<TaskResult>((resolve, reject) => {
setTimeout(() => {
reject(new Error("タスク失敗!"));
}, 1000);
});
// resultPromise.catch(() => {});
return {
get status() {
return resultPromise.then(result => result.status);
},
get value() {
return resultPromise.then(result => result.value);
},
get result() {
return resultPromise;
}
};
}
// taskのプロパティーに触れないと……
const task = runTask();
実行結果:
Unhandled Rejection: タスク失敗!
getterは個々のプロパティの遅延評価を実現しますが、元のPromiseのエラーハンドリングは行いません。そのため、プロパティにアクセスしなくても、元のPromiseがrejectされるとUnhandled Rejectionが発生してしまいます。
まとめ
部分的にPromiseな型を扱う際は:
-
Promise.then
で必要な値を取り出す - 元のPromiseに空の
catch
ハンドラーを追加して、Unhandled Rejectionを防ぐ -
getter
を使って遅延評価を実現する
これらの工夫により、実行時エラーを防ぎ、Promise
を部分的に扱えるようになります。特に、catchハンドラーと遅延評価の組み合わせが重要で、catchハンドラーは元のPromiseのエラーを処理し、getterは個々のプロパティの遅延評価を実現します。
最後までお読みくださりありがとうございました。この投稿が少しでもお役に立てば幸いです。Twitterでは、Qiitaに書かない技術ネタなどもツイートしているので、よかったらフォローお願いします→@suin