---
type: how-to
language: javascript
slug: async/timeout
title: "非同期処理に制限時間を付ける"
title_tag: "JavaScript のタイムアウト — Promise.race と AbortSignal"
summary: >
  Promise.race() で時間切れにできますが、遅いほうは裏で走り続けます。本当に止めるには
  AbortSignal が要ります。両方の書き方と、どちらを使うべきかの判断まで実行して確かめます。
description: >
  Promise.race() のタイムアウトは待つのをやめるだけで、処理自体は裏で走り続けます。
  AbortSignal で本当に止める書き方と、再試行や並列との組み合わせ方まで示します。
status: published
difficulty: 3
minutes: 9

versions:
  verified: "Node 22.22.3"
  since: "ES2015"
  deprecated: null
  removed: null

sources:
  - title: "Promise.race — ECMAScript® 2026 Language Specification"
    url: "https://tc39.es/ecma262/#sec-promise.race"
  - title: "AbortController — DOM Standard (WHATWG)"
    url: "https://dom.spec.whatwg.org/#interface-abortcontroller"
  - title: "AbortSignal — MDN"
    url: "https://developer.mozilla.org/ja/docs/Web/API/AbortSignal"

terms: [競争, 中止, 後始末]

links:
  related:
    - javascript/how-to/async/parallel
    - javascript/how-to/async/retry
    - javascript/reference/promise/promise
    - javascript/reference/timer/set-timeout

content_updated_at: 2026-09-09
published_at: 2026-09-09
---

## 答え

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

```js run
const sleep = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));

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

console.log(await withTimeout(sleep(1, '間に合った'), 50));

try {
  await withTimeout(sleep(50, 'おそい'), 1);
} catch (error) {
  console.log(error.message);
}
```
```output
間に合った
時間切れ
```

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

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

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

```js bad
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);
```
```output
時間切れ
裏で最後まで走った回数: 1
```

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

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

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

## 本当に止めるなら `AbortSignal`

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

```js run
function sleep(ms, signal) {
  return new Promise((resolve, reject) => {
    const id = setTimeout(resolve, ms);

    signal?.addEventListener('abort', () => {
      clearTimeout(id);
      reject(signal.reason);
    });
  });
}

const controller = new AbortController();

setTimeout(() => controller.abort(new Error('やめた')), 5);

try {
  await sleep(100, controller.signal);
} catch (error) {
  console.log('止まった:', error.message);
}
```
```output
止まった: やめた
```

- `AbortController` が[key:止めるための取っ手]
- `controller.signal` を、止めたい処理に渡す
- `controller.abort()` で、聞いている全員に伝わる

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

```js run
const controller = new AbortController();

console.log(controller.signal.aborted);

controller.abort();

console.log(controller.signal.aborted, controller.signal.reason.name);
```
```output
false
true AbortError
```

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

```js run
function sleep(ms, signal) {
  return new Promise((resolve, reject) => {
    const id = setTimeout(resolve, ms);

    signal?.addEventListener('abort', () => {
      clearTimeout(id);
      reject(signal.reason);
    });
  });
}

try {
  await sleep(100, AbortSignal.timeout(5));
} catch (error) {
  console.log(error.name + ': ' + error.message);
}
```
```output
TimeoutError: The operation was aborted due to timeout
```

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

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

## どちらを使うか

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

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

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

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

```js run
function classify(error) {
  if (error.name === 'TimeoutError') return '時間切れ';
  if (error.name === 'AbortError') return '中止した';
  return 'そのほかの失敗';
}

const controller = new AbortController();

controller.abort();

console.log(classify(controller.signal.reason));

const timer = AbortSignal.timeout(0);

await new Promise((resolve) => setTimeout(resolve, 10));

console.log(classify(timer.reason));
console.log(classify(new Error('通信できない')));
```
```output
中止した
時間切れ
そのほかの失敗
```

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

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

```js run
const user = new AbortController();

const signal = AbortSignal.any([user.signal, AbortSignal.timeout(1000)]);

console.log(signal.aborted);

user.abort(new Error('利用者が押した'));

console.log(signal.aborted, signal.reason.message);
```
```output
false
true 利用者が押した
```

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

## 全体に上限を付ける

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

```js run
const sleep = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));

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

try {
  await withTimeout(Promise.all([sleep(1, 'a'), sleep(50, 'b')]), 10);
} catch (error) {
  console.log(error.message);
}

console.log(await withTimeout(Promise.all([sleep(1, 'a'), sleep(2, 'b')]), 50));
```
```output
全体が時間切れ
[ 'a', 'b' ]
```

→ [複数の非同期処理を同時に走らせる](/ja/javascript/how-to/async/parallel/)

## 後始末は `finally` に書く

```js run
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

let open = 0;

async function run(ms, limit) {
  open++;

  try {
    return await Promise.race([
      sleep(ms).then(() => '成功'),
      new Promise((unused, reject) => setTimeout(() => reject(new Error('時間切れ')), limit)),
    ]);
  } finally {
    open--;
    console.log('後始末した');
  }
}

try {
  await run(50, 5);
} catch (error) {
  console.log(error.message);
}

console.log('開いたまま:', open);
```
```output
後始末した
時間切れ
開いたまま: 0
```

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

## 遅れて来た結果を捨てる

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

```js run
const sleep = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));

let latest = 0;

async function search(keyword, delay) {
  const id = ++latest;
  const result = await sleep(delay, keyword);

  if (id !== latest) {
    console.log(`${keyword} は古いので捨てる`);
    return null;
  }

  return result;
}

const first = search('古い検索', 30);
const second = search('新しい検索', 1);

console.log(await second);
console.log(await first);
```
```output
新しい検索
古い検索 は古いので捨てる
null
```

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

## 再試行と組み合わせる

```js run
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

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

let attempt = 0;

async function unstable() {
  attempt++;
  await sleep(attempt < 3 ? 50 : 1);
  return `${attempt}回目で成功`;
}

for (let i = 0; i < 3; i++) {
  try {
    console.log(await withTimeout(unstable(), 10));
    break;
  } catch (error) {
    console.log(`${i + 1}回目: ${error.message}`);
  }
}
```
```output
1回目: 時間切れ
2回目: 時間切れ
3回目で成功
```

**1回あたりの上限**と、**やり直す回数の上限**は別に決めてください。
どちらか片方だけだと、いつまでも終わらない場合があります。
→ [失敗したら再試行する](/ja/javascript/how-to/async/retry/)

## まとめ

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