2022年2月6日

非同期/await

Promiseをより快適に扱うための特別な構文に「async/await」があります。驚くほど理解しやすく、使いやすいです。

非同期関数

asyncキーワードから始めましょう。関数の前にこのように置くことができます。

async function f() {
  return 1;
}

関数の前に「async」という単語を付けるということは、1つの簡単なことを意味します。関数は常にPromiseを返します。他の値は解決済みのPromiseに自動的にラップされます。

例えば、この関数は結果が1である解決済みのPromiseを返します。テストしてみましょう。

async function f() {
  return 1;
}

f().then(alert); // 1

…明示的にPromiseを返すこともできますが、それは同じです。

async function f() {
  return Promise.resolve(1);
}

f().then(alert); // 1

そのため、asyncは関数がPromiseを返すことを保証し、Promise以外のものをラップします。簡単ですね?しかしそれだけではありません。もう1つのキーワードawaitがあり、これはasync関数内でのみ機能し、非常にクールです。

Await

構文

// works only inside async functions
let value = await promise;

キーワードawaitは、そのPromiseが解決するまでJavaScriptを待たせ、その結果を返します。

1秒後に解決するPromiseの例を次に示します。

async function f() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("done!"), 1000)
  });

  let result = await promise; // wait until the promise resolves (*)

  alert(result); // "done!"
}

f();

関数の実行は(*)行で「一時停止」し、Promiseが解決されると再開され、resultはその結果になります。そのため、上記のコードは1秒後に「完了!」と表示されます。

強調しておきましょう。awaitは、Promiseが解決するまで関数の実行を文字通り中断し、Promiseの結果で再開します。これはCPUリソースを消費しません。なぜなら、JavaScriptエンジンはそれまでの間に他のタスクを実行できるからです。他のスクリプトを実行したり、イベントを処理したりなどです。

これは、promise.thenよりもPromiseの結果を取得するためのよりエレガントな構文です。そして、読み書きが簡単です。

通常の関数ではawaitを使用できません

非同期関数以外でawaitを使用しようとすると、構文エラーが発生します。

function f() {
  let promise = Promise.resolve(1);
  let result = await promise; // Syntax error
}

関数の前にasyncを付けるのを忘れると、このエラーが発生する可能性があります。前述のように、awaitasync関数内でのみ機能します。

Promiseチェイニング章のshowAvatar()の例を取り上げ、async/awaitを使用して書き直してみましょう。

  1. .then呼び出しをawaitに置き換える必要があります。
  2. また、それらが機能するために、関数をasyncにする必要があります。
async function showAvatar() {

  // read our JSON
  let response = await fetch('/article/promise-chaining/user.json');
  let user = await response.json();

  // read github user
  let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
  let githubUser = await githubResponse.json();

  // show the avatar
  let img = document.createElement('img');
  img.src = githubUser.avatar_url;
  img.className = "promise-avatar-example";
  document.body.append(img);

  // wait 3 seconds
  await new Promise((resolve, reject) => setTimeout(resolve, 3000));

  img.remove();

  return githubUser;
}

showAvatar();

かなりクリーンで読みやすいですね?以前よりはるかに優れています。

最新のブラウザでは、モジュール内で最上位レベルのawaitが許可されています

最新のブラウザでは、モジュール内であれば、最上位レベルのawaitは問題なく機能します。モジュールについては、記事モジュール入門で説明します。

例えば

// we assume this code runs at top level, inside a module
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();

console.log(user);

モジュールを使用していない場合、または古いブラウザをサポートする必要がある場合は、普遍的な方法があります。匿名の非同期関数にラップします。

このように

(async () => {
  let response = await fetch('/article/promise-chaining/user.json');
  let user = await response.json();
  ...
})();
awaitは「thenable」を受け入れます

promise.thenと同様に、awaitを使用すると、thenableオブジェクト(呼び出し可能なthenメソッドを持つオブジェクト)を使用できます。これは、サードパーティのオブジェクトがPromiseではない可能性がありますが、Promiseと互換性があります。.thenをサポートしていれば、awaitで使用するには十分です。

ここにデモThenableクラスがあります。下のawaitはそのインスタンスを受け入れます。

class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve);
    // resolve with this.num*2 after 1000ms
    setTimeout(() => resolve(this.num * 2), 1000); // (*)
  }
}

async function f() {
  // waits for 1 second, then result becomes 2
  let result = await new Thenable(1);
  alert(result);
}

f();

await.thenを持つ非Promiseオブジェクトを取得した場合、そのメソッドを呼び出し、組み込み関数resolverejectを引数として提供します(通常のPromise実行関数と同様です)。その後、awaitはそれらのいずれかが呼び出されるまで待ちます(上記の例では(*)行で発生します)。そして、結果で続行します。

非同期クラスメソッド

非同期クラスメソッドを宣言するには、asyncを前に付けるだけです。

class Waiter {
  async wait() {
    return await Promise.resolve(1);
  }
}

new Waiter()
  .wait()
  .then(alert); // 1 (this is the same as (result => alert(result)))

意味は同じです。返された値がPromiseであることを保証し、awaitを有効にします。

エラー処理

Promiseが正常に解決された場合、await promiseは結果を返します。しかし、拒否された場合、その行にthrowステートメントがあるかのように、エラーをスローします。

このコード

async function f() {
  await Promise.reject(new Error("Whoops!"));
}

…はこれと同じです。

async function f() {
  throw new Error("Whoops!");
}

実際には、Promiseが拒否されるまでに時間がかかる場合があります。その場合、awaitがエラーをスローするまで遅延が発生します。

通常のthrowと同様に、try..catchを使用してそのエラーをキャッチできます。

async function f() {

  try {
    let response = await fetch('http://no-such-url');
  } catch(err) {
    alert(err); // TypeError: failed to fetch
  }
}

f();

エラーが発生した場合、制御はcatchブロックにジャンプします。複数の行をラップすることもできます。

async function f() {

  try {
    let response = await fetch('/no-user-here');
    let user = await response.json();
  } catch(err) {
    // catches errors both in fetch and response.json
    alert(err);
  }
}

f();

try..catchがない場合、非同期関数f()の呼び出しによって生成されたPromiseは拒否されます。.catchを付加して処理できます。

async function f() {
  let response = await fetch('http://no-such-url');
}

// f() becomes a rejected promise
f().catch(alert); // TypeError: failed to fetch // (*)

.catchを追加し忘れると、未処理のPromiseエラーが発生します(コンソールで確認できます)。第Promiseによるエラー処理章で説明されているように、グローバルなunhandledrejectionイベントハンドラを使用して、そのようなエラーをキャッチできます。

async/awaitpromise.then/catch

async/awaitを使用する場合、awaitが待機を処理するため、.thenはほとんど必要ありません。そして、.catchの代わりに通常のtry..catchを使用できます。これは通常(常にではありませんが)、より便利です。

しかし、コードの最上位レベル、つまり非同期関数の外側では、構文上awaitを使用できません。そのため、上記の例の(*)行のように、最終的な結果またはフォールスルーエラーを処理するために.then/catchを追加するのが一般的な方法です。

async/awaitPromise.allと連携して機能します

複数のPromiseを待機する必要がある場合、それらをPromise.allでラップしてからawaitできます。

// wait for the array of results
let results = await Promise.all([
  fetch(url1),
  fetch(url2),
  ...
]);

エラーが発生した場合、通常どおり、失敗したPromiseからPromise.allに伝播し、その後、呼び出しの周りにtry..catchを使用してキャッチできる例外になります。

まとめ

関数の前にasyncキーワードを付けることで、2つの効果があります。

  1. 常にPromiseを返すようにします。
  2. awaitの使用を許可します。

Promiseの前にawaitキーワードを付けることで、JavaScriptはそのPromiseが解決するまで待ち、その後

  1. エラーの場合、例外が生成されます。その場所でthrow errorが呼び出された場合と同じです。
  2. そうでない場合は、結果を返します。

これらは組み合わさり、読み書きが容易な非同期コードを作成するための優れたフレームワークを提供します。

async/awaitを使用すると、promise.then/catchを書くことはめったにありませんが、それらがPromiseに基づいていることを忘れてはいけません。なぜなら、場合によっては(例えば、最外部スコープでは)これらのメソッドを使用する必要があるからです。また、多くのタスクを同時に待機する場合、Promise.allは便利です。

課題

Promiseチェイニング章のこのサンプルコードを、.then/catchの代わりにasync/awaitを使用して書き直してください。

function loadJson(url) {
  return fetch(url)
    .then(response => {
      if (response.status == 200) {
        return response.json();
      } else {
        throw new Error(response.status);
      }
    });
}

loadJson('https://javascriptinfo.dokyumento.jp/no-such-user.json')
  .catch(alert); // Error: 404

注記はコードの下にあります。

async function loadJson(url) { // (1)
  let response = await fetch(url); // (2)

  if (response.status == 200) {
    let json = await response.json(); // (3)
    return json;
  }

  throw new Error(response.status);
}

loadJson('https://javascriptinfo.dokyumento.jp/no-such-user.json')
  .catch(alert); // Error: 404 (4)

注記

  1. 関数loadJsonasyncになります。

  2. 内部のすべての.thenawaitに置き換えられます。

  3. このように、それを待機する代わりにresponse.json()を返すことができます。

    if (response.status == 200) {
      return response.json(); // (3)
    }

    その後、外部コードはそのPromiseが解決するまで待機する必要があります。私たちの場合、それは問題ではありません。

  4. loadJsonからスローされたエラーは.catchによって処理されます。非同期関数内にはないため、そこでawait loadJson(…)を使用することはできません。

以下に「rethrow」の例を示します。.then/catchの代わりにasync/awaitを使用して書き直してください。

そして、demoGithubUserの再帰をループに置き換えてください。async/awaitを使用すると、それは簡単にできます。

class HttpError extends Error {
  constructor(response) {
    super(`${response.status} for ${response.url}`);
    this.name = 'HttpError';
    this.response = response;
  }
}

function loadJson(url) {
  return fetch(url)
    .then(response => {
      if (response.status == 200) {
        return response.json();
      } else {
        throw new HttpError(response);
      }
    });
}

// Ask for a user name until github returns a valid user
function demoGithubUser() {
  let name = prompt("Enter a name?", "iliakan");

  return loadJson(`https://api.github.com/users/${name}`)
    .then(user => {
      alert(`Full name: ${user.name}.`);
      return user;
    })
    .catch(err => {
      if (err instanceof HttpError && err.response.status == 404) {
        alert("No such user, please reenter.");
        return demoGithubUser();
      } else {
        throw err;
      }
    });
}

demoGithubUser();

ここでは特別なトリックはありません。demoGithubUser内で.catchtry..catchに置き換え、必要な場所にasync/awaitを追加するだけです。

class HttpError extends Error {
  constructor(response) {
    super(`${response.status} for ${response.url}`);
    this.name = 'HttpError';
    this.response = response;
  }
}

async function loadJson(url) {
  let response = await fetch(url);
  if (response.status == 200) {
    return response.json();
  } else {
    throw new HttpError(response);
  }
}

// Ask for a user name until github returns a valid user
async function demoGithubUser() {

  let user;
  while(true) {
    let name = prompt("Enter a name?", "iliakan");

    try {
      user = await loadJson(`https://api.github.com/users/${name}`);
      break; // no error, exit loop
    } catch(err) {
      if (err instanceof HttpError && err.response.status == 404) {
        // loop continues after the alert
        alert("No such user, please reenter.");
      } else {
        // unknown error, rethrow
        throw err;
      }
    }
  }


  alert(`Full name: ${user.name}.`);
  return user;
}

demoGithubUser();

fという「通常の」関数があります。どのようにしてasync関数wait()を呼び出し、その結果をf内で使用できますか?

async function wait() {
  await new Promise(resolve => setTimeout(resolve, 1000));

  return 10;
}

function f() {
  // ...what should you write here?
  // we need to call async wait() and wait to get 10
  // remember, we can't use "await"
}

追伸 この課題は技術的には非常に簡単ですが、async/awaitを使い始めた開発者にとってはよくある質問です。

内部の動作を理解していることが役立つケースです。

async呼び出しをPromiseとして扱い、.thenを添付するだけです。

async function wait() {
  await new Promise(resolve => setTimeout(resolve, 1000));

  return 10;
}

function f() {
  // shows 10 after 1 second
  wait().then(result => alert(result));
}

f();
チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善点に関するご提案がありましたら、コメントする代わりにGitHub issueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 数単語のコードを挿入するには<code>タグを使用し、複数行の場合は<pre>タグで囲み、10行を超える場合は、サンドボックス(plnkrjsbincodepen…)を使用してください。