はじめに

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

Promise とは

  • Promise は非同期関数を扱うためのインターフェース
  • Promise は 悲運のピラミッド型コールバック (ネスト地獄)を根本的に解決してくれる
  • Promise ベースの関数(thenable な関数という)同士ならメソッドチェーンでつなげて書ける

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 になったら状態はもとに戻らない

Promise が保証すること

  • コールバックは現在の JavaScript イベントループの実行よりも前には呼び出されない

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

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

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 => v) // 101
  .catch(failureCallback);

async/await を使った場合

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

(async () => {
  try {
    let result = await doSomething()
    let newResult = (result) => result + 1
    console.log(newResult) // 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)
    }, new Promise())
  }
}

// 使用例
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.result(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.result(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()

NGパターン(2)

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

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

myOrder()

OKパターン

(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

2014contribution

@SotaSuzuki OKパターン1ですが、恐らく並列ではなく直列で処理されると思います。
getFoodgetDrinkの実行自体はすぐ行われますが、orderItemsが呼び出されるのは、
getFood()のPromiseが解決 -> getDrink()のPromiseが解決 -> orderItems()を呼び出すという順番になります。
(関数の引数は左から順に処理されので左が解決されるまでは右へ移動しない。single var patternでのカンマ区切りの処理と同じです。)
https://qiita.com/SotaSuzuki/items/f23199e864cba47455ce#ok%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B31

await の理解度をもう1段階上げる - Qiita 2018-05-01 11-35-55.png

OKパターン1は意味的には次のように書いたのと同じです。

const getFood = () => {
  return new Promise(resolve => {
    setTimeout(resolve, 5000, '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)
  })
}

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

myOrder()

なので、次のような感じになるかなと思います。

  • OKパターン1: getFood -> getDrink > orderItems
  • OKパターン2: (getFood, getDrink) > orderItems
2014contribution

2. try/catch を使わない

await が抜けている気がします。

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

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

asyncFuncNoTryCatch() // Some error occured
614contribution

OKパターン1でもgetFoodとgetDrinkは並列に動きますね。
Promise内部のコールバック関数は即座に実行されます。
その後のawaitで直列になります。

672contribution

@azu さん
指摘して頂きありがとうございます。
上のOKパターン(1)について自分でも試してみたところ直列で実行されているようでした。
指摘して頂いた該当箇所を修正いたしましたので時間がある時にご確認頂けますと幸いです。

672contribution

@kurogelee さん
コメントありがとうございます。
getFoodとgetDrinkは並列に動くようですが、その後 orderItems 内で引数を処理する際に直列になるようですね。
例としてあまり良いものでは無いかもしれませんね、該当箇所は消してもよい気がしてきました。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.