Contents
Promise の基本概念と主要 API
Promise は「将来的に値が返ってくる」ことを約束したオブジェクトで、非同期処理の結果を統一的に扱えるように設計されています。
本セクションでは 状態遷移 と 主要メソッド(then / catch / finally) の挙動を整理し、実務で正しく使うためのポイントを解説します。
状態(pending・fulfilled・rejected)とライフサイクル
Promise は生成直後は pending です。
- resolve(value) が呼ばれると fulfilled に遷移し、value が結果として渡されます。
- reject(reason) が呼ばれると rejected に遷移し、reason がエラー情報になります。
状態は一度決まると変更できません(イミュータブル)。この不変性が チェーン可能 な理由です。
詳細は Zenn の解説を参照 → https://zenn.dev/divsawa/articles/20260201_js-promise-async-await
※外部リンクは定期的に有効性(200 OK)を確認し、アクセス不能になった場合は代替 URL に差し替えてください。
then / catch / finally の使い方と戻り値
then(onFulfilled, onRejected) は fulfilled 時に onFulfilled、rejected 時に onRejected を呼び出します。
catch(onRejected) は then(undefined, onRejected) と同等で、エラーハンドリングをシンプルに記述できます。
重要なのは then が返すのは新しい Promise である点です。これにより次のような直列チェーンが可能になります。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// 基本的なチェーン例(エラーは最後の catch に集約) fetch(url) .then(res => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }) .then(data => console.log('data →', data)) .catch(err => { // ここでネットワークエラー・JSON パースエラーなどを捕捉 console.error('fetch error:', err); }); |
finally(callback) は結果に関係なく必ず実行され、返す Promise は前段の状態(fulfilled / rejected)をそのまま受け継ぎます。
Qiita の図解解説 → https://qiita.com/ktdatascience/items/801b039d6438160edc4f
※リンク切れリスクに備えて、定期的にアクセス確認ツール(例: curl -I)でステータスコードをチェックしてください。
new Promise での手動実装と直列チェーン
この章ではコールバック API を Promise に変換 するパターンと、エラーハンドリングを強化した 直列処理のベストプラクティスを紹介します。
new Promise の構文と resolve / reject の実例(タイマーラッパーなど)
まずは setTimeout を Promise で包むシンプルな例です。エラー発生時に明示的に reject することで、呼び出し側で統一的に捕捉できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
/** * 指定ミリ秒だけ待機し、完了したらその値を返す Promise。 * キャンセルや例外が必要な場合は clearTimeout と reject を組み合わせる。 */ function delay(ms) { return new Promise((resolve, reject) => { if (typeof ms !== 'number' || ms < 0) { reject(new TypeError('delay expects a non‑negative number')); return; } const id = setTimeout(() => resolve(ms), ms); // キャンセル用ハンドラ(外部から clearTimeout が呼ばれたときの例) // (実務では AbortController と同様にシグナルを渡すことが多い) // return () => { clearTimeout(id); reject(new Error('cancelled')); }; }); } |
次に Node.js のコア API fs.readFile を Promise 化した例です。エラーは必ず reject に流すため、呼び出し側で try / catch が機能します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const fs = require('fs'); /** * ファイルを非同期に読み取り、テキストとして解決する Promise。 * @param {string} path - 読み込むファイルパス * @param {BufferEncoding} [encoding='utf8'] */ function readFilePromise(path, encoding = 'utf8') { return new Promise((resolve, reject) => { if (!path) { reject(new Error('Path is required')); return; } fs.readFile(path, encoding, (err, data) => { if (err) reject(err); else resolve(data); }); }); } |
Promise チェーンによる直列処理とエラーハンドリングのベストプラクティス
以下は delay → ファイル読み込み → JSON パース を順に行う例です。各 then が新しい Promise を返すので、途中で例外が起きても最終的な catch で一括捕捉できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
delay(1000) .then(ms => { console.log(`waited ${ms} ms`); return readFilePromise('./data.json'); }) .then(json => { try { // JSON.parse の例外はここで捕捉し、reject に変換 const obj = JSON.parse(json); console.log('parsed →', obj); return obj; } catch (e) { // 手動で reject させることでチェーン全体の catch に流す return Promise.reject(new SyntaxError('Invalid JSON')); } }) .catch(err => { // ここで全ステップの例外を一括処理 console.error('process error:', err); // 必要に応じて再スローして上位へ伝搬可能 // throw err; }) .finally(() => { console.log('process finished'); }); |
同様の直列例は Qiita に掲載 → https://qiita.com/ktdatascience/items/2fc46c542355067e100a
※リンクの有効性は半年ごとに自動スクリプトでチェックし、404 が返ったら別記事へ差し替える運用を推奨します。
複数の Promise を同時制御するメソッドと実務活用例
ここでは 並列処理 に適した組み込みメソッド(Promise.all, allSettled, race, any)の特徴と、実際にプロジェクトで採用すべきシナリオを解説します。
Promise.all と allSettled の違いと使い分け(並行 API 呼び出し)
Promise.all(iterable) は 全てが fulfilled した場合のみ結果配列を返し、1 つでも rejected があれば即座にエラーとなります。大量リクエストの トランザクション的 な処理に向きます。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
const usersP = fetch('/api/users').then(r => r.json()); const postsP = fetch('/api/posts').then(r => r.json()); Promise.all([usersP, postsP]) .then(([users, posts]) => { console.log(`取得件数: users=${users.length}, posts=${posts.length}`); }) .catch(err => { // いずれかが失敗したらここに来る console.error('batch failed:', err); }); |
対照的に Promise.allSettled は各 Promise の 成功/失敗ステータス をすべて取得でき、部分的に結果を利用したいダッシュボード等で有用です。
|
1 2 3 4 5 6 7 8 |
Promise.allSettled([usersP, postsP]) .then(results => { results.forEach((r, i) => { if (r.status === 'fulfilled') console.log(`data[${i}]`, r.value); else console.warn(`error[${i}]`, r.reason); }); }); |
実務では「全体成功が前提のバッチ処理」→
all、「一部だけでも表示したい UI」→allSettledを選択します。
Promise.race と any の特徴とユースケース(タイムアウト・最速レスポンス取得)
Promise.race(iterable) は 最初に決定された(fulfilled か rejected)Promise をそのまま返すため、タイムアウト実装や「最速の API」取得に適しています。
|
1 2 3 4 5 6 7 8 9 10 |
function timeout(ms) { return new Promise((_, reject) => { setTimeout(() => reject(new Error('timeout')), ms); }); } Promise.race([fetch('/slow-api'), timeout(3000)]) .then(res => res.json()) .catch(err => console.error('race error:', err)); |
Promise.any(iterable) は 最初に fulfilled したものだけを返し、すべてが rejected の場合のみ AggregateError をスローします。フォールバック先が複数ある場合の「どれか一つ成功すれば良い」シナリオで便利です。
|
1 2 3 4 5 6 7 |
const primary = fetch('/primary'); const secondary = fetch('/secondary'); Promise.any([primary, secondary]) .then(res => res.json()) .catch(err => console.error('all failed:', err)); |
メソッド比較は codingls.com のまとめが参考 → https://codingls.com/js/6189/
※外部リンクは CI パイプラインで HEAD リクエストを走らせ、200 系ステータス以外の場合は Slack に通知する仕組みを導入すると運用が楽です。
async/await との比較・変換テクニック
Promise ベースのコードを async/await に置き換えると可読性が向上しますが、例外処理や finally の扱いに注意が必要です。本節では変換手順と実務で気をつけるポイントを示します。
Promise ベースのコードを async/await に置き換える例
先ほどの直列チェーンを async 関数へリファクタリングした完全版です。エラーハンドリングは try / catch で包括的に行い、finally は必ず実行されます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
/** * delay → readFilePromise → JSON.parse の一連処理を async/await で記述。 */ async function processData() { try { const ms = await delay(1000); console.log(`waited ${ms} ms`); const json = await readFilePromise('./data.json'); let obj; try { obj = JSON.parse(json); } catch (e) { // JSON パース失敗は独自エラーとして再スロー throw new SyntaxError('Invalid JSON in data.json'); } console.log('parsed →', obj); } catch (err) { // delay、readFile、JSON.parse のいずれかで捕捉された例外がここに集約 console.error('processData error:', err); } finally { console.log('process finished'); } } processData(); |
変換のポイント
| Promise | async/await |
|---|---|
then |
await |
catch |
try / catch |
finally |
同名の finally ブロック |
同期風に書くテクニックと注意点(エラーハンドリング、finally の扱い)
- 必ず try/catch:
awaitが reject された場合は例外としてスローされるため、忘れずに捕捉します。 - finally は必ず実行:
returnやthrowがあっても finally 内のコードは走りますが、returnの結果は上書きできません(注意)。 - 配列を順次処理したいときは
for await…of:並列にしたくない場合はループ内でawaitを使うだけで安全です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/** * URL 配列を順番に取得し、失敗したものはスキップして警告表示。 */ async function fetchAllSequential(urls) { const results = []; for (const url of urls) { try { const res = await fetch(url); if (!res.ok) throw new Error(`HTTP ${res.status}`); results.push(await res.json()); } catch (e) { console.warn('skip', url, e.message); } } return results; } |
「同期的に制御したい」解説は watashi.xyz に詳述 → https://watashi.xyz/js-promise/
※リンク先が将来的に消失しないよう、GitHub のミラーリポジトリや自前のドキュメントへも同内容を保存しておくと安全です。
実務で役立つ高度な実装例とデバッグ手法
この章では fetch に対するリトライ・タイムアウト・キャンセル、汎用的な Promise ラッパー、そして開発時に便利なデバッグテクニックを紹介します。
fetch と組み合わせたリトライ・タイムアウト・キャンセル(AbortController)
以下のユーティリティは 最大 3 回までリトライ、5 秒のタイムアウト、そして外部からの AbortController によるキャンセルに対応しています。エラーハンドリングを細かく分岐させている点が実務向きです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
/** * fetch のラッパー: リトライ・タイムアウト・Abort 対応。 * * @param {string} url - 取得対象 URL * @param {object} [options={}] - fetch オプション(signal 等) * @param {number} [retries=3] - 最大リトライ回数 * @param {number} [timeoutMs=5000] - タイムアウトミリ秒 * @returns {Promise<any>} JSON データ(成功時)または例外をスロー */ async function fetchWithRetry(url, options = {}, retries = 3, timeoutMs = 5000) { const controller = new AbortController(); const { signal } = controller; // タイムアウト用 Promise(Abort と同時に reject) const timeoutPromise = new Promise((_, reject) => { const id = setTimeout(() => { controller.abort(); // fetch 側も中断 reject(new Error('request timed out')); }, timeoutMs); // Abort が呼ばれたらタイマーをクリアしておく signal.addEventListener('abort', () => clearTimeout(id)); }); for (let attempt = 1; attempt <= retries; attempt++) { try { const response = await Promise.race([ fetch(url, { ...options, signal }), timeoutPromise, ]); if (!response.ok) throw new Error(`HTTP ${response.status}`); return await response.json(); // 正常終了 } catch (err) { // AbortError は再試行しない if (err.name === 'AbortError') { console.error('request aborted:', err); throw err; } // 最終試行か致命的エラーならそのままスロー if (attempt === retries) { console.error(`fetch failed after ${retries} attempts`); throw err; } console.warn(`retry ${attempt}/${retries} after error: ${err.message}`); } } } |
使用例(キャンセルボタン付き UI)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<button id="cancelBtn">Cancel</button> <script type="module"> import { fetchWithRetry } from './fetchUtil.js'; const abortCtrl = new AbortController(); document.getElementById('cancelBtn').onclick = () => abortCtrl.abort(); fetchWithRetry('/api/data', { signal: abortCtrl.signal }) .then(data => console.log('got data →', data)) .catch(err => console.error('final error:', err)); </script> |
カスタムユーティティティとしての Promise ラッパー
ファイル読み込みシミュレーション(テスト用モック)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/** * 任意の文字列を遅延させて返すモック Promise。 * * @param {string} content - 返したいデータ * @param {number} [delayMs=200] - 人為的な遅延時間 */ function mockReadFile(content, delayMs = 200) { return new Promise(resolve => setTimeout(() => resolve(content), delayMs)); } // テスト例 mockReadFile('{"id":1}', 100) .then(JSON.parse) .then(obj => console.log('mock object →', obj)) .catch(err => console.error('mock error:', err)); |
データベースクエリラッパー(Node の pg ライブラリ想定)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
const { Client } = require('pg'); const client = new Client(); /** * PostgreSQL クエリを Promise 化。 * * @param {string} sql - 実行する SQL 文 * @param {Array<any>} [params=[]] - プレースホルダー用パラメータ配列 */ function queryPromise(sql, params = []) { return new Promise((resolve, reject) => { client.query(sql, params, (err, res) => { if (err) reject(err); else resolve(res.rows); }); }); } // 呼び出し例 async function getActiveUsers() { try { const rows = await queryPromise('SELECT * FROM users WHERE active=$1', [true]); return rows; } catch (e) { console.error('DB error:', e); throw e; // 上位へ再送出 } } |
デバッグテクニック:console.time、async stack trace の取得、VSCode デバッガ活用
| 手法 | 効果 | 使用例 |
|---|---|---|
console.time / console.timeEnd |
非同期ブロックの実行時間を測定でき、パフォーマンスボトルネックを可視化 | js\nconsole.time('api');\nawait fetch('/slow');\nconsole.timeEnd('api');\n |
| Async Stack Trace | エラー発生箇所を非同期チェーン全体で追跡でき、原因特定が容易になる | js\nError.stackTraceLimit = 50; // デフォルトは10\nthrow new Error('test');\n |
| VSCode の「Async Call Stacks」デバッグ | ブレークポイントで Promise の内部状態や呼び出し元を確認でき、ステップ実行が快適になる | launch.json に "runtimeArgs": ["--inspect-brk"] を設定し、VSCode から起動 |
詳細は codingls.com の実務パターン解説ページが参考になります → https://codingls.com/js/6189/
まとめと次のアクション
- Promise の基本:状態遷移は一方向で不変、
then / catch / finallyがチェーンを可能にします。 - 手動実装:
new Promiseで既存コールバック API を包み、エラーハンドリングは必ずrejectに流す設計が重要です。 - 同時制御:
Promise.all/allSettledは全体成功と部分結果取得の使い分け、race/anyはタイムアウトや最速応答取得に活用します。 - async/await への移行:
try / catch / finallyに置き換えるだけで可読性が向上し、エラーは一元管理できます。 - 高度実装例:リトライ・タイムアウト・AbortController を組み合わせた
fetchWithRetry、汎用的な DB/ファイルラッパー、そしてconsole.timeや VSCode デバッガによるデバッグ手法で信頼性を高めます。
実務導入のチェックリスト
1. すべての外部リンクは CI パイプラインで定期的にステータスコードを検証する。
2. Promise ラッパーは必ずrejectにエラーオブジェクト(Error派生)を渡し、呼び出し側でcatchできるようにする。
3. エラーハンドリングが簡素化されていないかコードレビュー時に確認し、必要なら詳細例外情報を付与する。
これらのパターンを自プロジェクトに組み込めば、非同期ロジックの可読性・保守性・信頼性が格段に向上します。ぜひ本稿で紹介したコードスニペットをベースに、実際の開発フローへすぐに適用してみてください。