非同期処理に制限時間を付ける

Promise.race() で時間切れにできますが、遅いほうは裏で走り続けます。本当に止めるには AbortSignal が要ります。両方の書き方と、どちらを使うべきかの判断まで実行して確かめます。

実行して検証済み処理系Node 22.22.3対応ES2015難易度●●●○○目安9更新

答え

Promise.race() で、処理と時計を競争させます。

example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
間に合った 時間切れ
javascript
const sleep = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));

let finished = 0;

function slow() {
return sleep(30, 'おそい').then((value) => {
finished++;
return value;
});
}

function withTimeout(promise, ms) {
return Promise.race([
promise,
new Promise((unused, reject) => setTimeout(() => reject(new Error('時間切れ')), ms)),
]);
}

try {
await withTimeout(slow(), 5);
} catch (error) {
console.log(error.message);
}

await sleep(60);

console.log('裏で最後まで走った回数:', finished);
時間切れ 裏で最後まで走った回数: 1

Promise.race() は、最初に決着したほうの結果をそのまま返します。 → Promise — then / catch / finally と async / await

ただし、処理は止まっていない

ここを誤解したまま使っている書き方が多くあります。

example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
止まった: やめた
example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
false true AbortError

Promise.race() がやめたのは待つことだけです。 中の処理は最後まで走ります。

困るのはこういう場面です。

  • 時間切れにしたのに、通信の料金だけかかる
  • 再試行を重ねるたびに、裏で走るものが増えていく
  • 遅れて返ってきた結果で、新しい表示が古い値に上書きされる

本当に止めるなら AbortSignal

中止できる作りになっているものだけが、本当に止まります。

example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
TimeoutError: The operation was aborted due to timeout
example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
中止した 時間切れ そのほかの失敗
  • AbortController止めるための取っ手
  • controller.signal を、止めたい処理に渡す
  • controller.abort() で、聞いている全員に伝わる

abort() に何も渡さないと、決まった形の例外になります。

example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
false true 利用者が押した
example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
全体が時間切れ [ 'a', 'b' ]

時間切れだけなら1行で書ける

example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
後始末した 時間切れ 開いたまま: 0
example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
新しい検索 古い検索 は古いので捨てる null

AbortSignal.timeout(ms) は、その時間で自動的に止まる合図を作ります。 自分で setTimeout を書く必要がありません。

fetch()signal を受け取れるので、そのまま渡せば通信ごと止まります。このページの実行環境では通信を行わないため、待つ処理で示しています。

どちらを使うか

Promise.race() AbortSignal
待つのをやめる できる できる
処理そのものを止める できない できる
相手の対応 要らない signal を受け取る作りが要る
書く量 少ない 多い

signal を渡せる相手なら、AbortSignal を使ってください。 渡せない相手(自分で書いた重い計算など)には Promise.race() しかありませんが、 そのときは裏で走り続けることを承知したうえで使います。

何で終わったのかを見分ける

時間切れ・利用者の中止・ふつうの失敗は、分けて扱ってください。

example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
1回目: 時間切れ 2回目: 時間切れ 3回目で成功

利用者が自分で中止したのに「失敗しました」と出すのは、いちばん嫌われる作りです。 中止は失敗ではありません。

利用者の中止と時間切れを両方待つ

AbortSignal.any() は、どれか1つでも止まったら止まる合図を作ります。 「利用者が押したら止める」と「30秒で諦める」を、1つにまとめられます。

全体に上限を付ける

1件ずつではなく、まとめて上限を付けたいことがあります。

複数の非同期処理を同時に走らせる

後始末は finally に書く

時間切れでも中止でも成功でも、finally は必ず動きます。 「読み込み中」の表示を消す処理は、ここに書いてください。 → Error と try / catch / finally / throw

遅れて来た結果を捨てる

Promise.race() しか使えない場面では、結果を採用してよいかを自分で見張ります。

後から始めたものが先に終わったら、前のものは捨てます。 これをしないと、入力するたびに表示が行ったり来たりします。

再試行と組み合わせる

1回あたりの上限と、やり直す回数の上限は別に決めてください。 どちらか片方だけだと、いつまでも終わらない場合があります。 → 失敗したら再試行する

まとめ

  • 時間切れは Promise.race() で作れる
  • やめられるのは「待つこと」だけ。処理は裏で走り続ける
  • 本当に止めるなら AbortControllersignal
  • 時間切れだけなら AbortSignal.timeout(ms) が短い
  • signal を渡せない相手なら、遅れて来た結果を捨てる仕組みを自分で持つ
  • 中止は失敗ではない。 AbortErrorTimeoutError を分けて扱う
  • 利用者の中止と時間切れは AbortSignal.any() でまとめられる
  • 「読み込み中」を消す処理は finally に書く
  • 1回あたりの上限と、やり直す回数の上限は別に決める

この記事の根拠

  1. Promise.race — ECMAScript® 2026 Language Specificationtc39.es
  2. AbortController — DOM Standard (WHATWG)dom.spec.whatwg.org
  3. AbortSignal — MDNdeveloper.mozilla.org

掲載しているコード例は、公開前に読み手が押したときに動くのと同じもので実行して出力を突き合わせています。結果はリポジトリに残しています。

このページの原文(Markdown)