JavaScript: Promiseやasync/awaitのメモ

5 min read読了の目安(約4900字 1

Promise ってなあに

非同期処理を捌くやつ。成功パターンならresolve、失敗パターンならrejectでコールバックする。thencatchfinallyを用いたチェーンメソッドで処理を繋いでいく。then は処理成功パターンで戻り値を受け取ることができる。catch は処理失敗パターンで処理の途中で throw された Error を受け取ることができる。catch を置くと、それまでの then で転けた Error を拾える。finally は成功しても失敗しても必ず実行される。finally の中で return してもその戻り値は受け取れない。

シンプルな Promise の実装

100%成功するPromise
const successPromise = () => {
  return Promise.resolve("OK");
}
100%失敗するPromise
const failurePromise = () => {
  return Promise.reject(new Error("NG"));
}
1秒後に1/2の確率でOKを返すPromise
const simplePromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    if (Math.floor(2 * Math.random()) % 2 === 0) {
      resolve("OK");
    } else {
      reject(new Error("NG"));
    }
  }, 1000);
});
チェーンメソッドの例
simplePromise
  .then((res) => {
    // 成功パターンの戻り値がくるよ
    console.log(res);
  })
  .catch((error) => {
    // 失敗パターンのErrorがくるよ
    console.error(error);
  })
  .finally(() => {
    console.log("絶対呼ばれるよ");
  });
関数やアロー関数にして引数を渡すこともできる
// 関数パターン
function simplePromise(flag) {
  return new Promise((resolve, reject) => {
    if (flag === true) {
      resolve("OK");
    } else {
      reject(new Error("NG"));
    }
  });
}

// アロー関数パターン
const simplePromise = (flag) =>
  new Promise((resolve, reject) => {
    if (flag === true) {
      resolve("OK");
    } else {
      reject(new Error("NG"));
    }
  });

ポイント

  • then の中で return したら自動的に Promise.resolve()される。
hogePromise()
  .then(() => {
    return "Hello";
  })
  .then((res) => {
    console.log(res); // Helloが出力される
  });
  • then の中で throw したら自動的に Promise.reject()される。
hogePromise()
  .then(() => {
    throw new Error("例外です");
  })
  .catch((error) => {
    console.error(error); // 例外ですが出力される
  });
  • 途中で失敗した場合、その後の then はすっ飛ばされて catch があればそこに Error が流れる。
hogePromise()
  .then(() => {
    throw new Error("例外です");
  })
  .then(() => {
    console.log("🐓"); // 出力されない
  })
  .then(() => {
    console.log("🐀"); // 出力されない
  })
  .catch((error) => {
    console.error(error); // 例外ですが出力される
  });

async/await ってなあに

Promise と同様に非同期処理を捌くやつ。Promise はチェーンメソッドで処理を繋いでいくが、async/await では手続き的に処理を書ける。要は、async がついたスコープ内で await をつけて非同期処理を呼ぶと、処理が終わるまで待って結果を返してくれる。いくつかの非同期処理の結果が出揃ってから次の処理をしたい場合に有効。async 関数は自動的に Promise 型の戻り値を返す。つまり、async 関数を呼び出す場合、Promise と同じ扱い方をする必要がある。async 関数内で Error が throw された場合は、自動的に Promise.reject()される。

シンプルな async/await

// 関数パターン
async function simpleAsync() {
  const result = await successPromise();
  console.log(result);
}

// アロー関数パターン
const simpleAsync = async () => {
  const result = await successPromise();
  console.log(result);
};

async/await で例外をキャッチする

try/catch を使うと非同期処理で転けた時の Error を捉えられる。

const tryCatchAsync = async () => {
  try {
    await failurePromise();
  } catch (error) {
    console.error(error); // failurePromiseのErrorが出力される
  }
};

ポイント

  • async/await と Promise は組み合わせて使える。
少し複雑な例
const hogeAsync = async () => {
  try {
    // thenの中の結果を待つことが可能
    const json = await getArrayPromise().then((res) => res.json());
    return "array" in json ? json.array : [];
  } catch (error) {
    // errorをここで捌かずに戻り値のPromiseに流せる
    throw error;
  }
};

hogeAsync()
  .then((array) => {
    console.log(array.join(", "));
  })
  .catch((error) => {
    console.error(error);
  });

配列を基に非同期処理を直列で実行する方法

for 文で async/await を使う方法と reduce で Promise を使う方法がある。

// 一秒後に引数をechoする非同期処理
const echoAfter1sec = (arg) =>
  new Promise((resolve, _) => {
    setTimeout(() => {
      console.log(arg);
      resolve();
    }, 1000);
  });

const words = ["Apple", "Banana", "Cherry"];

// for文を使うパターン
const seriesRun = async () => {
  for (let word of words) {
    await echoAfter1sec(word);
  }
};
seriesRun();

// reduceを使うパターン
words.reduce((promiseChain, word) => {
  return promiseChain.then(() => {
    return echoAfter1sec(word);
  });
}, Promise.resolve());
// ↑これ自体も戻り値はPromise

配列を基に非同期処理を並列で実行する方法

Promise.all を使うと、複数の Promise を渡して並列実行できる。また、全ての非同期処理が終わったらその結果を Array で取得できる。途中で非同期処理が失敗した場合は、処理をそこで止めて Error を reject する。その場合は then で止まらないためいくつか成功した処理があってもその分の結果は取得できない。(つまり、通信系で沢山画像を収集する場合などは、Error にせずに成功パターンの例外として処理するといい)

// 一秒後に引数の一文字目を返す非同期処理
const getFirstChar = (str) =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      if (str.length === 0) {
        reject(new Error("string is empty."));
      } else {
        resolve(str.slice(0, 1));
      }
    }, 1000);
  });

const parallelRun = (words) => {
  // Promiseの配列を作る
  const promises = words.map((word) => getFirstChar(word));
  Promise.all(promises)
    .then((results) => {
      console.log(results);
    })
    .catch((error) => {
      console.error(error);
    });
};

// 成功パターン
parallelRun(["Apple", "Banana", "Cherry"]);
// => [ 'A', 'B', 'C' ]

// 失敗パターン
parallelRun(["Apple", "", "Cherry"]);
// => Error: string is empty.