Help us understand the problem. What is going on with this article?

Promiseの使い方、それに代わるasync/awaitの使い方

JavaScriptのasync/awaitはPromiseを使った非同期感たっぷりのコードを同期処理っぽくしてくれるが、async/awaitを理解するには、Promiseも知る必要がある。そこで、Promiseからおさらいしておこうと思う。

構文

まず基本的な構文。どうやってPromiseを作成するのか?

function 非同期的な関数(成功時コールバック, 失敗時コールバック) {
  if (...) {
    成功時コールバック(成果)
  } else {
    失敗時コールバック(問題)
  }
}

//          ↓executor
new Promise(function (resolve, reject) {
  非同期的な関数(
    (成果) => resolve(成果), // 成功時コールバック関数
    (問題) => reject(問題),  // 失敗時コールバック関数
  )
})
  • Promiseクラスをnewして使う。
  • コンストラクタの引数
    • 引数はexecutor1つだけ
    • executorFunction
      • resolveexecutorに渡ってくる関数
        • 非同期な処理が成功したとき、resolveに成果となる値を渡す。
      • rejectexecutorに渡ってくる関数
        • 非同期な処理が失敗したとき、rejectに問題となるエラーオブジェクトなどを渡す。

例: axiosとPromise

/**
 * サーバの死活監視。ステータスコードを返す。
 * @returns {Promise.<number>}
 */
function getServerStatusCode() {
    return new Promise(function(resolve, reject) {
        axios
            .get("https://httpbin.org/status/200")
            .then(response => resolve(response.status))
            .catch(error => reject(error.response.status))
    });
}

getServerStatusCode()
    .then(statusCode => console.log("生きてる", statusCode))
    .catch(statusCode => console.error("死んでる", statusCode))
  • CodePen → Axios and Promise
  • :warning: これはちょっと冗長なコードになっている。もっとすっきり書く方法は後述する。

Promise.resolve

new Promise(function (resolve, reject) {
    resolve(1)
})

このコードは下のコードに書き換えることができる

Promise.resolve(1)

なので、thenなどとメソッドチェーンもできる

Promise.resolve(1)
    .then(value => {
        console.log(value)
    })

asyncとawait - 非同期処理を同期処理っぽく書く

ご覧の通り、非同期処理の結果はthenにコールバックを与えないと処理できない。同期処理のコードと比べると一手間必要だ。

/**
 * @returns {Promise.<number>}
 */
function 非同期処理() {
    return Promise.resolve(1)
}

/**
 * @returns {number}
 */
function 同期処理() {
    return 1
}

function main() {
    console.log(1 + 非同期処理()) // 1[object Promise]
    console.log(1 + 同期処理()) // 2
    // Promiseの結果の処理はthenが必要
    非同期処理().then(value => console.log(1 + value)) // 2
}

main()

asyncとawaitを使うと同期処理っぽく書けるようになる。

  • await → Promiseの値が取り出されるまで待つ。
  • asyncawaitキーワードを使っている関数のあたまに付ける必要がある。
/**
 * @returns {Promise.<number>}
 */
function 非同期処理() {
    return Promise.resolve(1)
}

async function main() {
    console.log(1 + 非同期処理()) // 1[object Promise]
    console.log(1 + await 非同期処理()) // 2
}

main()

例: axiosとasync/awaitによる同期的な書き方

/**
 * サーバの死活監視。ステータスコードを返す。
 * @returns {Promise.<number>}
 */
async function getServerStatusCode() {
  try {
    return (await axios.get("https://httpbin.org/status/500")).status
  } catch (error) {
    throw error.response.status
  }
}

getServerStatusCode()
    .then(statusCode => console.log("生きてる", statusCode))
    .catch(statusCode => console.error("死んでる", statusCode))
  • 前述したaxiosとPromiseの例をasync/awaitに一部書き換えたもの。
  • CodePen → Axios and async

async functionはPromiseを返す

asyncはawaitとセットで使う必要はない。単体の機能としては関数をPromiseを返す関数に変身させること。

/**
 * @returns {Promise.<number>}
 */
function getNumber1() {
    return Promise.resolve(1)
}

/**
 * @returns {Promise.<number>}
 */
async function getNumber2() {
    return 1
}

console.log(getNumber1()) // Promise { 1 }
console.log(getNumber2()) // Promise { 1 }

async function内で例外を飛ばしたらどうなるか?

async function内で例外をthrowした場合でも、あくまで関数の戻り値はPromise型。例外も.catch()で処理できる。

/**
 * @returns {Promise.<void>}
 */
async function throwError() {
    throw "err"
}

throwError().catch(console.log) // errと出力される

async function内でPromiseを返したらどうなるか?

戻り値がPromiseなら、そのまま返される。なので、Promiseが二重になることはない。

/**
 * async functionなのにPromiseをreturnする関数
 * @returns {Promise.<number>}
 */
async function promise() {
    return Promise.resolve(1)
}

/**
 * @returns {Promise.<number>}
 */
async function number() {
    return 1
}

promise().then(console.log) // 1
number().then(console.log) // 1

これはasync functionの性質というより、Promiseの性質のようだ。Promise.resolveをいれこにしてもPromiseが多重にならない。

console.log(
    Promise.resolve(
        Promise.resolve(
            Promise.resolve(1)
        )
    )
) //=> Promise { 1 }

finallyはPromiseにもある?

async/awaitだとfinallyで成功時・失敗時どちらでも共通の処理を実行できる。

/**
 * @returns {Promise.<void>}
 */
async function getStatusCode() {
    try {
        console.log('started')
        const response = await axios.get('https://httpbin.org/status/200')
        console.log('success', response.status)
    } catch (error) {
        console.log('error', error.response.status)
    } finally {
        console.log('finished') // 共通処理
    }
}

これと同じことはPromiseでもできるのだろうか?

function getStatusCode() {
    console.log('started')
    return axios.get('https://httpbin.org/status/200')
        .then(response => {
            console.log('success', response.status)
        })
        .catch(error => {
            console.log('error', error.response.status)
        })
        .finally(_ => {
            console.log('finished') // !?!?
        })

結論を言うと、無いことはないがPromise.prototype.finally() - JavaScript | MDNに書いてある通り、2019-05-28現時点でChromeやEdgeをはじめとした多くのブラウザが対応しているが、Internet Explorerは未対応である。なお、babel-polyfilが入っていれば、finallyも使えるようだ。

Promiseでもそんなにコードの保守性悪くないのでは?

ここまで見てきたサンプルコードでは、Promiseを使っていてもそこまで保守性を損ねそうにない。では、どういうときにasync & awaitのほうがコードがシンプルになるのだろうか。それは、2段以上Promiseのthenが重なるときだと思う。例えば次のようなコード。

function main() {
    getX().then(x => {
        getY().then(y => {
            console.log(x + y)
        })
    })
}

function getX() {
    return Promise.resolve(1)
}

function getY() {
    return Promise.resolve(2)
}

main()

これをasync & awaitに書き換えるとすっきりする。

async function main() {
    const x = await getX()
    const y = await getY()
    console.log(x + y)
}

async function getX() {
    return 1
}

async function getY() {
    return 2
}

main()

今日現在のPromiseにはmapが生えていない

  • Promiseにはmapメソッドが生えていない。
  • サードパーティでそれを可能にするライブラリがあるかも? 探してない。

参考文献


最後までお読みくださりありがとうございました。Twitterでは、Qiitaに書かない技術ネタなどもツイートしているので、よかったらフォローお願いします:relieved:Twitter@suin

suin
Qiita 4位/TypeScript入門書執筆中/TypeScripterのための座談会「YYTypeScript」主催/『実践ドメイン駆動設計』書籍邦訳レビュア/分報Slack考案/YYPHP主催/CodeIQマガジン執筆/株式会社クラフトマンソフトウェア創設/Web自動テスト「ShouldBee」の開発/TypeScript/DDD/OOP
https://yyts.connpass.com/
shouldbee
開発者向けテスト支援サービスShouldBeeを開発・運営するスタートアップ(onlab第8期)
http://shouldbee.at
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした