---
title: "非同期関数の書き方｜async宣言・await式・TypeScriptの型定義とdelay関数の実装"
url: "https://www.issoh.co.jp/tech/details/3794/"
published: 2024-10-09
updated: 2026-08-04
categories: ["TypeScript"]
publisher: "株式会社一創"
---

# 非同期関数の書き方｜async宣言・await式・TypeScriptの型定義とdelay関数の実装

非同期関数でつまずく場所は、だいたい3つに絞られます。`async` を付けた関数が何を返すのか、`await` をどこに書けるのか、TypeScriptでその戻り値をどう型で表すのか。ここではJavaScriptとTypeScriptの実際のコードで、この3点と、待機処理でよく必要になるdelay関数の実装までを扱います。非同期処理そのものの考え方（同期処理との違い、イベントループ、言語ごとの設計思想）は[非同期処理とは？同期処理との違いから実装方式まで実装者目線で解説](/tech/details/13590/)に、エラーの捕まえ方と並列実行の制御は[JavaScriptの非同期エラーハンドリングと並列実行｜try/catch・Promise.all・allSettledの使い分け](/tech/details/3691/)にまとめてあります。

## まとめ｜非同期関数の書き方で押さえる要点

- `async` を付けた関数は、何を `return` しても戻り値がPromiseに包まれます。`return 1` は `Promise<number>` です。
- `await` を書けるのは非同期関数の中と、ESモジュールのトップレベルだけです。CommonJS（`require`）のファイル直下では構文エラーになります。
- TypeScriptの戻り値型は `Promise<T>` と書き、受け取り側で `T` を取り出すときは `Awaited<T>` を使います。
- delay（sleep）は `setTimeout` をPromiseで包む3行で書けます。Node.jsなら `node:timers/promises` の `setTimeout` がそのまま使えます。
- 同期処理を `async` で包むだけの関数は、呼び出し側の型と実行順序をPromiseに巻き込む分だけ損です。非同期にする理由がないなら付けません。

## 非同期関数の宣言｜asyncを付けた関数が返すもの

### 戻り値が自動でPromiseに包まれる規則

`async` の効果は2つだけです。関数の戻り値をPromiseで包むことと、その中で `await` を使えるようにすること。数値を返しても文字列を返しても、呼び出し側が受け取るのはPromiseです。

```
async function loadCount() {
  return 1;                // 呼び出し側には Promise<number> が返る
}

const p = loadCount();     // p は Promise。ここでは中身の 1 は取り出せない
console.log(p);            // Promise { 1 }
console.log(await p);      // 1

async function loadUser() {
  return fetch('/api/user').then((r) => r.json());  // Promise を返しても二重には包まれない
}
```

返した値がPromise（正確には `then` を持つオブジェクト）だった場合、二重には包まれず、解決済みの値に平坦化されます。`return await something` と `return something` がほぼ同じ結果になるのはこのためです。違いが出るのは `try/catch` の内側で、`await` を省くとその関数のcatchでは捕まらなくなります。

### コールバック関数からPromise・async/awaitへの移行理由

コールバック関数とは、別の関数に引数として渡し、処理が終わった時点で呼び出してもらう関数のことです。非同期処理の結果を受け取る手段として長く使われてきましたが、結果を使って次の非同期処理を呼ぶ、という連鎖を書くとネストが深くなります。

```
// コールバック方式：エラー処理が各段に散らばる
getUser(id, (err, user) => {
  if (err) return handle(err);
  getOrders(user.id, (err, orders) => {
    if (err) return handle(err);
    getInvoice(orders[0], (err, invoice) => { /* ... */ });
  });
});

// async/await：手続きの順序がそのまま縦に並ぶ
const user = await getUser(id);
const orders = await getOrders(user.id);
const invoice = await getInvoice(orders[0]);
```

コールバックが消えたわけではありません。`addEventListener` や配列の `map` のように、「呼ばれる回数が1回とは限らない」処理は今もコールバックが担当します。1回だけ完了を知らせる処理がPromiseに置き換わった、と捉えると使い分けを間違えません。

## await式の書ける場所と再開のタイミング

### 非同期関数の中と、ESモジュールのトップレベル

`await` が書けるのは `async` を付けた関数の中です。加えて、ESモジュール（`type: "module"` 指定または `.mjs`）に限りファイル直下でも書けます。これがトップレベルawaitで、CommonJSのファイルでは使えません。

```
// ESモジュール（package.json に "type": "module"）
const config = await loadConfig();   // ファイル直下で await できる
export default config;

// CommonJS では SyntaxError。即時実行関数で包む必要がある
(async () => {
  const config = await loadConfig();
})();
```

トップレベルawaitを使ったモジュールは、それをimportした側の評価も待たせます。起動時の設定読み込みのように「無いと先に進めない」処理に向き、リクエストごとの取得のような繰り返し呼ばれる処理には向きません。

### await後の再開順序とマイクロタスク

`await` は関数の実行をそこで中断し、同期的なコードを先に最後まで走らせます。再開されるのはPromiseが解決した後で、しかも `setTimeout` より先です。両者はキューが別で、Promiseの継続（マイクロタスク）が優先されます。

```
async function main() {
  console.log('1');
  await null;              // 値でも await できる。ここで一旦抜ける
  console.log('3');
}

setTimeout(() => console.log('4'), 0);
main();
console.log('2');
// 出力順： 1 → 2 → 3 → 4
```

`await null` のように解決済みの値を待った場合でも、その場で続きが走ることはありません。「awaitを書いた行より後ろは、必ず一度イベントループに制御が戻ってから実行される」と覚えておくと、ログの順序が想定と違う場面で迷わなくなります。

## TypeScriptでの非同期関数の型定義

### 戻り値はPromise<T>、取り出すときはAwaited<T>

TypeScriptでは `async` 関数の戻り値型を `Promise<T>` と書きます。`T` をそのまま書くとコンパイルエラーになります。逆に、既存の関数の戻り値からPromiseを剥がした型が欲しいときは `Awaited<T>` を使います。

```
type User = { id: string; name: string };

async function fetchUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  return (await res.json()) as User;
}

// 戻り値の中身の型だけを取り出す
type FetchedUser = Awaited<ReturnType<typeof fetchUser>>;  // User

// 配列で待つ場合も中身の型は保たれる
const users: User[] = await Promise.all(ids.map((id) => fetchUser(id)));
```

`res.json()` の戻り値は `any` です。`as User` はコンパイラを黙らせるだけで、実行時に形が違えばそのまま通ります。外部APIの応答を型で受けるなら、`unknown` で受けてからバリデーションライブラリで検証する方が安全です。型注釈は実行時の保証ではない、という前提を崩さないことが、非同期処理では特に効きます。

### catch変数の型とコンパイラ設定

`strict` を有効にすると `useUnknownInCatchVariables` も入り、`catch` の変数は `unknown` になります。`e.message` を直接読むとエラーになるため、絞り込みが要ります。

```
try {
  await fetchUser('u1');
} catch (e: unknown) {
  if (e instanceof Error) {
    console.error(e.message);       // ここでは Error に絞り込まれている
  } else {
    console.error(String(e));       // 文字列など Error 以外も throw できる
  }
}
```

コンパイラ本体は2026年7月8日公開の7.0.2が最新で、Goで書き直されたネイティブ版に移行しています（移行の要点は[TypeScript 7.0とは？Goネイティブ移植で型チェックを約10倍速くする仕組みを解説](/tech/details/13413/)を参照）。async/awaitの構文自体は変わっていませんが、`target` を `ES2017` より低く設定するとasync関数はジェネレータへ変換されるため、出力コードの姿は設定で変わります。捕まえた後の扱い（再throw・原因の連結・上位への伝播）は[JavaScriptの非同期エラーハンドリングと並列実行｜try/catch・Promise.all・allSettledの使い分け](/tech/details/3691/)で扱います。

## delay関数の実装｜setTimeoutをPromiseにする書き方

### 3行で書けるdelay関数

JavaScriptに標準のsleepはありません。`setTimeout` をPromiseで包むのが定番で、TypeScriptなら引数と戻り値に型を付けるだけです。

```
// TypeScript
export const delay = (ms: number): Promise<void> =>
  new Promise((resolve) => setTimeout(resolve, ms));

// 値を返したい場合
export const delayValue = <T>(ms: number, value: T): Promise<T> =>
  new Promise((resolve) => setTimeout(() => resolve(value), ms));

await delay(1000);   // 1秒待ってから次の行へ
```

`setTimeout` の指定時間は「最短の待ち時間」です。タイマーが切れてもコールバックはキューに並ぶだけなので、同期処理が詰まっていれば実際の再開はそれより後になります。ミリ秒単位の精度が要る処理をdelayで組まないでください。

### Node.js標準のnode:timers/promises版setTimeout

Node.jsには待機用のPromise版タイマーが標準で入っています。自作のdelayを置く必要はありません。第3引数で `AbortSignal` を渡せば、待機中の中断もできます。

```
import { setTimeout as sleep } from 'node:timers/promises';

await sleep(1000);                       // 1秒待つ
const value = await sleep(1000, 'done'); // 解決値を指定できる

// 中断できる待機
const ac = new AbortController();
setTimeout(() => ac.abort(), 300);       // グローバル側のsetTimeout
try {
  await sleep(1000, undefined, { signal: ac.signal });
} catch (e) {
  console.log('待機を中断しました');       // AbortError
}
```

ブラウザ側には同等の標準APIがないため、自作のdelayを使います。中断が要る場合は `AbortSignal` の `abort` イベントで `resolve` を呼ぶ実装を足すか、そもそも待機ではなくタイムアウト付きの処理として組み直す方が素直です。

### delayでポーリングを書くときの上限

「終わるまで一定間隔で問い合わせる」処理をdelayで書く場合、回数の上限を必ず持たせます。上限のないループは、相手が壊れているときに永久に待ち続けます。

```
async function waitForReady(id: string, maxAttempts = 10): Promise<boolean> {
  for (let i = 0; i < maxAttempts; i++) {
    if (await isReady(id)) return true;
    await delay(1000 * 2 ** i);   // 1秒、2秒、4秒... と間隔を広げる
  }
  return false;
}
```

間隔を毎回同じにすると、復旧直後に全クライアントが同時に殺到します。上の例のように待ち時間を倍にしていく指数バックオフと、全体のタイムアウトを組み合わせるのが実務での既定です。リトライ側の設計（何回まで、どの例外なら再試行してよいか、冪等性の担保）は[JavaScriptの非同期エラーハンドリングと並列実行｜try/catch・Promise.all・allSettledの使い分け](/tech/details/3691/)で扱います。

## asyncを付けるべきでない4つの場面

非同期関数は書けば書くほど良いものではありません。次の4つは、付けた側が損をする典型です。

```
// 1. 同期処理を async で包むだけ：呼び出し側が必ず await を強いられる
async function toUpper(s: string) { return s.toUpperCase(); }

// 2. forEach に async を渡す：Promise が捨てられ、待たれない
items.forEach(async (i) => { await save(i); });   // 保存完了前に次の行へ進む
for (const i of items) { await save(i); }         // 直列でよければこちら

// 3. コンストラクタ内で await したい：構文上できない。ファクトリ関数にする
class Client {
  private constructor(readonly token: string) {}
  static async create() { return new Client(await getToken()); }
}
```

4つ目はテストでの固定待ちです。`await delay(500)` で「たぶん終わっているはず」を待つテストは、CIの負荷が上がった日に落ちます。完了イベントや戻り値のPromiseなど、明示的な合図を待ってください。1と2は特に見落とされやすく、1は型が `Promise<string>` になることで呼び出し側の連鎖まで非同期に引きずり、2は「保存されていないのにレスポンスを返す」というデータの取りこぼしにつながります。

## よくある質問

### 非同期関数とは何ですか？

処理の完了を待たずに呼び出し元へ制御を返す関数で、JavaScriptでは `async` を付けて宣言します。戻り値は必ずPromiseになり、中で `await` を書けます。実行が「速くなる」わけではなく、待ち時間の間に他の処理を進められるようになる、という性質の違いです。

### TypeScriptでdelay（sleep）はどう書きますか？

`const delay = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));` と書き、`await delay(1000)` で使います。Node.jsで動かすコードなら `node:timers/promises` の `setTimeout` をimportすれば自作は不要です。

### awaitはトップレベルで使えますか？

ESモジュールなら使えます。`package.json` に `"type": "module"` を書いたプロジェクトか `.mjs` ファイルが対象です。CommonJSでは構文エラーになるため、即時実行の非同期関数で包みます。

### コールバック関数とは何ですか？

他の関数に引数として渡し、処理の完了時やイベント発生時に呼び出してもらう関数です。1回だけ完了を知らせる用途はPromiseとasync/awaitに置き換わりましたが、複数回発火するイベント処理では今もコールバックが標準の書き方です。

### async関数の戻り値をそのまま変数に入れられないのはなぜですか？

戻り値がPromiseだからです。`const n = loadCount();` の `n` はPromiseオブジェクトで、中身の値ではありません。`await` を付けるか `.then()` で受け取ります。`console.log` に `Promise { <pending> }` と出るのはこの状態です。

## 関連記事

- [非同期処理とは？同期処理との違いから実装方式まで実装者目線で解説](/tech/details/13590/)
- [JavaScriptの非同期エラーハンドリングと並列実行｜try/catch・Promise.all・allSettledの使い分け](/tech/details/3691/)
- [TypeScript 7.0とは？Goネイティブ移植で型チェックを約10倍速くする仕組みを解説](/tech/details/13413/)
- [Node.js 26の新機能・変更点とLTSスケジュール総まとめ](/tech/details/12014/)
- [useSWRとは？基本的な使い方とfetcher・オプション設定をReactで解説](/tech/details/4694/)

---

出典: [非同期関数の書き方｜async宣言・await式・TypeScriptの型定義とdelay関数の実装](<https://www.issoh.co.jp/tech/details/3794/>)（株式会社一創）
