JavaScript
promise
async
await

Promise と async/await の理解度をもう1段階上げる

はじめに

表題のモチベーションで書き上げた備忘録に加筆・修正したものを記事にしました。
記事を読んで下さった方の Promise, async/await の理解度が 1LV でもアップしてくれたら嬉しく思います。

Promise とは

  • Promise は非同期関数を扱うためのインターフェース
  • Promise は 悲運のピラミッド型コールバック (ネスト地獄)を根本的に解決してくれる
  • thenable と呼ばれる then メソッドを有するオブジェクトを resolve の第一引数に入れることで Promise オブジェクトへ変換することが可能
// thenable を Promise オブジェクトへ変換する
const converted = Promise.resolve({
  then: (onFulfilled) => onFulfilled('be promise')
})
converted.then(value => console.log(value)) // be promise

async/await とは

  • async/await は Promise のシンタックスシュガーとして用いられる
  • async/await は Promise を同期的なコードのように書ける

Promise の書き方とメソッドの簡単な説明

Promise の最も基本的なコード

const promise = new Promise((resolve, reject) => resolve('解決した'))
const unResolvedPromise = new Promise((resolve, reject) => reject('エラー発生'))

promise
  .then((value) => value) // 解決した

unResolvedPromise
  .then((value) => value) 
  .catch((errorMessage) => new Error(errorMessage)) // error: エラー発生
  .then((value) => value) // エラー発生

then と catch

then と catch はプロミスにおける最重要事項の一つです。これらは promise を返すため、メソッドチェーンを繋げることができます。

then

Promise#then(onFulfilled, onRejected)

  • Promise に成功ハンドラ(onFulfilled) と失敗ハンドラ(onRejected) を付加する
  • 2つの引数を持ち、それらはそれぞれ Promise の状態が Fulfilled の時と Rejected の時のコールバック関数である
  • 返り値は promise

onFulfilled(value)

  • 一つの引数 value を持つ
  • value は promise にラップされた値など

onRejected(reason)

  • 一つの引数 reason を持つ
  • reason は rejected となった理由など

catch

Promise#catch(onRejected)

  • then(undefined, onRejected) のショートカットとして機能する
  • 返り値は promise
  • 引数の onRejected は then と参照

静的メソッド

Promise.resolve

下記コードのエイリアスです。渡した値で Fulfilled される promise オブジェクトを返します。

new Promise((resolve) => resolve(/* value */))

Promise.reject

下記コードのエイリアスです。

new Promise((resolve, reject) => reject(/* reason */))

Promise.all

  • 引数に Promise 関数の配列を取る実行
  • 返り値は Promise
  • 引数に取った Promise で一つでもエラーが発生した場合はその rejected されたエラーが返る
  • エラーが発生しなかった場合は「value にそれぞれの resolve の結果が入った配列」の Promise が返る
Promise.all(promises).then(allFulfilled, firstRejected)

Promise.race

  • 引数に Promise 関数の配列を取り実行する
  • 引数にとった Promise 関数のいずれかが fullFilled または rejected になった時点でその Promise を返す
Promise.race(promises).then(firstFulfilled, firstRejected)

Promise の仕様的なところ

async/await をちゃんと使えるようになるには Promise が内部でどう働いているかある程度わかっている必要があります。

Promise は状態をもつ

インスタンス化された promise のオブジェクトには以下の 3 つの状態が存在します。

  1. Fulfilled
  2. Rejected
  3. Pending
  • promise のオブジェクトの状態は不変である
  • 一度 Fulfilled か Rejected になったら Pending には戻らない
  • Fulfilled, Rejected は Settled(落ち着いた状態, 不変の状態)ともいう
  • Pending から Settled になったら状態はもとに戻らない

渡された関数の実行タイミング

  • すぐに実行されるのではなく、マイクロタスクのキューに入れられる
  • このキューは現在のイベントループの終わりに空になる

async/await の書き方

console.log(101) するコードを (1)then catch, (2)async/await の2パターン書きました。
doSomething 関数内でエラーが発生した場合は failureCallback にエラーが渡されてログに出すようにしています。

then と catch を使った場合

const doSomething = () => Promise.resolve(100)
const failureCallback = (e) => console.log(e)

doSomething()
  .then(v => v + 1)
  .then(v => console.log(v)) // 101
  .catch(failureCallback);

async/await を使った場合

// ※ 即時関数で囲っています
const doSomething = () => Promise.resolve(100)
const failureCallback = (e) => console.log(e)

(async () => {
  try {
    let result = await doSomething()
    result = result + 1
    console.log(result) // 101
  } catch (e) {
    failureCallback(e)
  }
})();

サンプルコード集

筆者の場合 Promise, async/await は説明やコードを読んでいるだけではいまいち理解できなかったので、実際にコードを書いて動かして理解を深めました。その過程で特に残しておきたい(忘れた時に後から見たい)と思ったコードを載せております。

古いコールバックを使った API をラップする

setTimeout をラップします。これはコールバックのAPIをラップしたもっともシンプルな例かもしれません。

// setTimeout を Promise にする
const wait = ms => {
  return new Promise((resolve) => setTimeout(resolve, ms))
}
wait(1000).then(() => console.log('logged after 1000ms'))

複数の関数を直列に合成する

const composeAsync = (...funcs) => {
  return x => {
    return funcs.reduce((acc, val) => {
      return acc.then(val)
    }, Promise.resolve(x))
  }
}

// 使用例
const succ = n => n + 1
const multiply = x => y => x * y

const transformData = composeAsync(succ, succ, multiply(3))
transformData(2)
  .then(v => console.log(v)) // 12

async/await と Promise のコードを比較してみる

async function 内で console.log(100)、async function を呼ぶときに console.log(100) するコードで比較してみます。両コードの挙動は全く同じです。
async/await 使った方が圧倒的に書きやすく、またコードを見た時にどういう動きをしそうか、直感的に理解しやすいと思いました。

async/await

const ret100 = () => Promise.resolve(100)

const asyncFunc = async () => {
  let result = await ret100()
  result += await ret100()
  console.log(result) // log.1 200
  return result // Promise を返す
}
asyncFunc()
    .then(value => console.log(value)) // log2. 200

Promise

const ret100 = () => Promise.resolve(100)

const asyncFuncPromise = () => {
  let result = 0
  return ret100()
    .then(value => {
      result += value
      return ret100()
    })
    .then(value => {
      result += value
      console.log(result) // log.1 200
      return result
    })
}

asyncFuncPromise()
  .then(value => console.log(value)) // log.2 200

async/await のエラー処理の記述パターン

下記はサンプルコード内で使用している errorPromise 関数です。

const errorPromise = () => {
  return new Promise((resolve, reject) => {
    try {
      throw new Error('Some error occured')
      resolve('no error')
    } catch (err) {
      reject(err)
    }
  })
}

1. try/catch を使う

const asyncFuncTryCatch = async () => {
  try {
    const result = await errorPromise()
    console.log(result) // unreachable
  } catch (err) {
    console.log(err)
  }
}

asyncFuncTryCatch() // log. Some error occured

2. try/catch を使わない

const asyncFuncNoTryCatch = async () => {
  const result = await errorPromise()
    .catch(err => err)
  console.log(result)
}

asyncFuncNoTryCatch() // Some error occured

3. async function 内でエラーハンドリングしない

const asyncFuncHandleErrorOut = async () => {
  const result = await errorPromise()
  console.log(result) // unreachable
}

// async function を呼ぶ時にエラーハンドリング
asyncFuncHandleErrorOut()
  .catch(err => console.log(err)) // Some error occured

おまけ. Promise のエラーハンドリング

const asyncPromiseFunc = () => {
  return errorPromise()
    .then(value => value)
    .catch(err => err)
}

asyncPromiseFunc() // Some error occured

非同期関数を並列で実行させたいとき

非同期関数を並列で実行させたいときは Promise.all を使用する必要があります。

例えば、下記のようなコードがあり、 getFoodgetDrink を並列で実行し、両関数が完了した後に orderItems を実行したいとします。


const getFood = () => {
  return new Promise(resolve => {
    setTimeout(resolve, 1000, 'got food')
  })
}

const getDrink = () => {
  return new Promise(resolve => {
    setTimeout(resolve, 1000, 'got a drink')
  })
}

const orderItems = (...items) => {
  return new Promise(resolve => {
    setTimeout(() => {
      items.forEach((msg) => console.log(msg))
    }, 2000)
  })
}

NGパターン

下記コードは getFood > getDrink > orderItems のように直列で実行されてしまいます。

const myOrder = async () => {
  const msg1 = await getFood()
  const msg2 = await getDrink()
  orderItems(msg1, msg2)
}

myOrder()

OKパターン(1)

(getFood, getDrink) > orderItems のように並列で実行します。
NGパターン(1) 同様 getFood > getDrink > orderItems のように直列で実行されます。
@azu さん 指摘して頂きありがとうございます。)

※修正:20180501.1745
改めて確認したところ、(getFood, getDrink) > orderItems のように並列で実行されるようでした。
但し、 OKパターン(2) のように Promise.all を使用するのが一般的のようなので、そちらを使うのがベターと思われます。

 const myOrder = async () => {
  const msg1 = getFood()
  const msg2 = getDrink()
  orderItems(await msg1, await msg2)
}

myOrder()

OKパターン(2)

(getFood, getDrink) > orderItems のように getFoodgetDrink は並列で実行されます。一般的に使われているのはこちらのパターンのようです。

 const myOrder = async () => {
  const msg1 = getFood()
  const msg2 = getDrink()
  const msgs = await Promise.all([msg1, msg2])
  orderItems(...msgs)
}

myOrder()

おわりに

以上です。
async/await にしろ Promise にしろ、ドキュメントや記事(この記事のような)を読むだけでなく、自分で手を動かしトライ&エラーを繰り返すことって大事だなとあらためて思いました(どの技術の習得もそうですが・・)。

コードや文章の指摘、質問も頂けると助かります。
最後までお読み頂きありがとうございました。

参考資料

Next steps