JavaScript忍耐のプログラマー向け(ES2022版)
本書をサポートしてください:購入する または 寄付する
(広告、ブロックしないでください。)

41章 非同期関数



概して、非同期関数はPromiseを使用するコードにとってより良い構文を提供します。非同期関数を使用するには、そのためPromiseを理解する必要があります。前の章で説明されています。

41.1章 非同期関数:基本

次の非同期関数について考えてみましょう。

async function fetchJsonAsync(url) {
  try {
    const request = await fetch(url); // async
    const text = await request.text(); // async
    return JSON.parse(text); // sync
  }
  catch (error) {
    assert.fail(error);
  }
}

前の、かなり同期的に見えるコードは、Promiseを直接使用する次のコードと同等です。

function fetchJsonViaPromises(url) {
  return fetch(url) // async
  .then(request => request.text()) // async
  .then(text => JSON.parse(text)) // sync
  .catch(error => {
    assert.fail(error);
  });
}

非同期関数fetchJsonAsync()に関するいくつかの観察事項

fetchJsonAsync()fetchJsonViaPromises()の両方は、このようにまったく同じ方法で呼び出されます。

fetchJsonAsync('http://example.com/person.json')
.then(obj => {
  assert.deepEqual(obj, {
    first: 'Jane',
    last: 'Doe',
  });
});

  非同期関数は、Promiseを直接使用する関数と同様にPromiseベースです

外部から見ると、非同期関数とPromiseを返す関数との違いを判別することは事実上不可能です。

41.1.1節 非同期構成要素

JavaScriptには、同期の呼び出し可能なエンティティの次の非同期バージョンがあります。それらの役割は常に実際の関数またはメソッドのいずれかです。

// Async function declaration
async function func1() {}

// Async function expression
const func2 = async function () {};

// Async arrow function
const func3 = async () => {};

// Async method definition in an object literal
const obj = { async m() {} };

// Async method definition in a class definition
class MyClass { async m() {} }

  非同期関数 vs. 非同期関数

用語「非同期関数」と「async関数」の違いは微妙ですが重要です。

41.2章 非同期関数からの戻り値

41.2.1節 非同期関数は常にPromiseを返す

各非同期関数は常にPromiseを返します。

非同期関数内では、return(A行)を介して結果のPromiseを解決します。

async function asyncFunc() {
  return 123; // (A)
}

asyncFunc()
.then(result => {
  assert.equal(result, 123);
});

通常どおり、何も明示的に返さない場合、undefinedが返されます。

async function asyncFunc() {
}

asyncFunc()
.then(result => {
  assert.equal(result, undefined);
});

throw(A行)を介して結果のPromiseを拒否します。

async function asyncFunc() {
  throw new Error('Problem!'); // (A)
}

asyncFunc()
.catch(err => {
  assert.deepEqual(err, new Error('Problem!'));
});

41.2.2節 返されたPromiseはラップされない

非同期関数からPromise pを返す場合、pは関数の結果になります(あるいはむしろ、結果はpに「固定」され、まったく同じように動作します)。つまり、Promiseは別のPromiseにラップされません。

async function asyncFunc() {
  return Promise.resolve('abc');
}

asyncFunc()
.then(result => assert.equal(result, 'abc'));

次の状況では、すべてのPromise qが同様に扱われることを思い出してください。

41.2.3節 非同期関数の実行:同期開始、非同期解決(上級)

非同期関数は次のように実行されます。

結果pの解決の通知は、Promiseの場合常にそうであるように、非同期的に発生することに注意してください。

次のコードは、非同期関数が同期的に開始され(A行)、次に現在のタスクが終了し(C行)、次に結果のPromiseが非同期的に解決される(B行)ことを示しています。

async function asyncFunc() {
  console.log('asyncFunc() starts'); // (A)
  return 'abc';
}
asyncFunc().
then(x => { // (B)
  console.log(`Resolved: ${x}`);
});
console.log('Task ends'); // (C)

// Output:
// 'asyncFunc() starts'
// 'Task ends'
// 'Resolved: abc'

41.3章 await:Promiseの使用方法

await演算子は、非同期関数と非同期ジェネレーター(42.2章「非同期ジェネレーター」で説明)内でのみ使用できます。そのオペランドは通常Promiseであり、次の手順が実行されます。

awaitがさまざまな状態のPromiseをどのように処理するかについて、さらに読み進めてください。

41.3.1節 awaitと解決済みのPromise

オペランドが解決済みのPromiseである場合、awaitはその解決値を返します。

assert.equal(await Promise.resolve('yes!'), 'yes!');

Promise以外の値も許可されており、単に渡されます(非同期関数を一時停止せずに同期的に)。

assert.equal(await 'yes!', 'yes!');

41.3.2節 awaitと拒否されたPromise

オペランドが拒否されたPromiseである場合、awaitは拒否値をスローします。

try {
  await Promise.reject(new Error());
  assert.fail(); // we never get here
} catch (e) {
  assert.equal(e instanceof Error, true);
}

  演習:非同期関数によるFetch API

exercises/async-functions/fetch_json2_test.mjs

41.3.3節 awaitは浅い(コールバック内では使用できない)

非同期関数内にいて、awaitを使用して一時停止したい場合、その関数内で直接実行する必要があります。ネストされた関数(コールバックなど)内では使用できません。つまり、一時停止は浅いです。

たとえば、次のコードは実行できません。

async function downloadContent(urls) {
  return urls.map((url) => {
    return await httpGet(url); // SyntaxError!
  });
}

理由は、通常の矢印関数は本体内でawaitを許可しないためです。

では、非同期矢印関数を試してみましょう。

async function downloadContent(urls) {
  return urls.map(async (url) => {
    return await httpGet(url);
  });
}

しかし、これも機能しません。.map()(したがってdownloadContent())は、(ラップされていない)値を含む配列ではなく、Promiseを含む配列を返します。

1つの可能な解決策は、Promise.all()を使用してすべてのPromiseをアンラップすることです。

async function downloadContent(urls) {
  const promiseArray = urls.map(async (url) => {
    return await httpGet(url); // (A)
  });
  return await Promise.all(promiseArray);
}

このコードを改善できますか?はい、できます。A行では、awaitを介してPromiseをアンラップし、すぐにreturnを介して再ラップしています。awaitを省略すると、非同期矢印関数も必要なくなります。

async function downloadContent(urls) {
  const promiseArray = urls.map(
    url => httpGet(url));
  return await Promise.all(promiseArray); // (B)
}

同じ理由で、B行でもawaitを省略できます。

41.3.4節 モジュールの最上位レベルでのawaitの使用 [ES2022]

モジュールの最上位レベルでawaitを使用できます。たとえば、

let lodash;
try {
  lodash = await import('https://primary.example.com/lodash');
} catch {
  lodash = await import('https://secondary.example.com/lodash');
}

この機能の詳細については、27.14章「モジュールでの最上位レベルのawait [ES2022]」を参照してください。

  演習:非同期的なマッピングとフィルタリング

exercises/async-functions/map_async_test.mjs

41.4章 (上級)

残りのセクションはすべて上級者向けです。

41.5章 並行処理とawait

次の2つの小節では、ヘルパー関数paused()を使用します。

/**
 * Resolves after `ms` milliseconds
 */
function delay(ms) {
  return new Promise((resolve, _reject) => {
    setTimeout(resolve, ms);
  });
}
async function paused(id) {
  console.log('START ' + id);
  await delay(10); // pause
  console.log('END ' + id);
  return id;
}

41.5.1節 await:非同期関数を順次実行する

複数の非同期関数の呼び出しの前にawaitを付ける場合、これらの関数は順次実行されます。

async function sequentialAwait() {
  const result1 = await paused('first');
  assert.equal(result1, 'first');
  
  const result2 = await paused('second');
  assert.equal(result2, 'second');
}

// Output:
// 'START first'
// 'END first'
// 'START second'
// 'END second'

つまり、paused('second')は、paused('first')が完全に終了した後にのみ開始されます。

41.5.2節 await:非同期関数を並行して実行する

複数の関数を同時に実行したい場合は、ツールメソッドPromise.all()を使用できます。

async function concurrentPromiseAll() {
  const result = await Promise.all([
    paused('first'), paused('second')
  ]);
  assert.deepEqual(result, ['first', 'second']);
}

// Output:
// 'START first'
// 'START second'
// 'END first'
// 'END second'

ここでは、両方の非同期関数が同時に開始されます。両方が解決されると、awaitは解決値の配列、または少なくとも1つのPromiseが拒否された場合は例外を返します。

40.6.2節「並行処理のヒント:操作の開始時期に注目する」から、重要なのはPromiseベースの計算を開始するタイミングであり、結果を処理する方法ではないことを思い出してください。したがって、次のコードは前のコードと同じくらい「並行」です。

async function concurrentAwait() {
  const resultPromise1 = paused('first');
  const resultPromise2 = paused('second');
  
  assert.equal(await resultPromise1, 'first');
  assert.equal(await resultPromise2, 'second');
}
// Output:
// 'START first'
// 'START second'
// 'END first'
// 'END second'

41.6章 非同期関数の使用方法に関するヒント

41.6.1節 「ファイア・アンド・フォーゲット」の場合はawaitは不要

Promiseベースの関数を使用する場合は、awaitは必要ありません。返されたPromiseが解決されるまで一時停止して待機したい場合にのみ必要です。非同期操作を開始したいだけの場合、それは必要ありません。

async function asyncFunc() {
  const writer = openFile('someFile.txt');
  writer.write('hello'); // don’t wait
  writer.write('world'); // don’t wait
  await writer.close(); // wait for file to close
}

このコードでは、終了するタイミングを気にしないため、.write()を待機しません。ただし、.close()が完了するまで待機する必要があります。

注:.write()の各呼び出しは同期的に開始されます。これにより、競合状態が防止されます。

41.6.2節 awaitして結果を無視しても意味がある場合がある

結果を無視する場合でも、awaitを使用する方が理にかなう場合があります。たとえば、

await longRunningAsyncOperation();
console.log('Done!');

ここでは、awaitを使用して長時間実行される非同期操作を結合しています。これにより、ロギングがその操作が完了したに確実に実行されます。