2017/10/20更新

[フロントエンド] ES7のasync/awaitを使って、Promiseを同期的に処理する

このエントリーをはてなブックマークに追加      

こんにちは、@yoheiMuneです。
JavaScriptといえば非同期プログラミングですが、async/await構文を使うと、同期的に処理を実行できるようになります。今日は、そんなとても便利なasync/awaitについてブログに書きたいと思います。
画像


目次




Async/Awaitとは

ECMAScriptで定義されている(2017/10現在はディスカッション中)、新しいJavaScriptの構文です。これを使うことで、Promiseベースの処理を同期的に処理することが可能になります。例えば以下のようなコードを書くことができます。
async function main () {

    // Promiseベースの処理を、同期的に処理できる.
    const val1 = await sleep(100, 'Hello')
    console.log('val1:', val1)  // val1: Hello
}

function sleep (msec, val) {
    return new Promise((resolve, reject) => {
        setTimeout(resolve, msec, val)
    })
}
Promiseベースの処理を同期的に実装できるのは、非常に素晴らしいですよね。
Async/Awaitと同等の機能は、generatoryieldを用いて実現できますが、それを言語レベルでサポートしています(専門的にいうとAsync/Awaitはgeneretaor/yield糖衣構文(Syntax sugar)です)。

今日はそんな便利なasyncawaitの使い方を見てみたいと思います。



async/awaitの使い方

いくつかのユースケースに分けて、使い方を見てみたいと思います。


その前に

最初の例でも使いましたが、これ以降のサンプルで、以下のPromiseを返す関数を例に、説明していきます。
// 指定秒数待った後に、引数で指定されたvalをresolveで返却します.
function sleep (msec, val) {
    return new Promise((resolve, reject) => {
        setTimeout(resolve, msec, val)
    })
}


基本的な使い方

基本的な使い方は、asyncを付与した関数を定義し、その中でawaitを利用します。そうすることで、Promiseを同期的に扱うことができます。
// awaitを使いたい場合には、必ずasyncを付与する.
async function main () {

    // awaitで呼び出すことで、同期的に処理できる.
    const val1 = await sleep(100, 'Hello')
    console.log('val1:', val1)

    return 'OK'
}
大切なことは、async関数の中でawaitを使うことです。async関数以外でawaitを記述すると、構文エラーになります。

そして、asyncが付与された関数はPromiseを返却します。main関数の終了はthen関数で補足することができます。
    main().then(result => {
        console.log('result:', result)  // result: OK
    })
このように、asyncawaitを用いると、Promiseチェーンを実装しなくても、非同期処理が実装できてすごく素敵です。


複数のawaitを直列で呼び出す

以下のように行を変えて、複数のawaitを書くと、それぞれの処理を直列で実行します。
// Sequencial.
const t1 = Date.now()
const val2 = await sleep(500, 1)
const val3 = await sleep(500, 2)
const val4 = await sleep(500, 3)
console.log(`val2+val3+val4=${val2 + val3 + val4}`) // val2+val3+val4=6
console.log(`${Date.now() - t1}ms`) // 1,500ms

// 1,500ms <= 500msずつ3回、直列で呼び出されている
500ms待機する処理を3つ実行しているので、1500msかかっていることがわかります。


複数のawaitを並列で呼び出す

複数のawaitを並列に呼び出すこともできます。並列で呼び出すには、以下のように演算式の中でawaitをするか、
// 並列にawaitを処理する
const t2 = Date.now()
const val5 = sleep(500, 1)
const val6 = sleep(500, 2)
const val7 = sleep(500, 3)
const val8 = await val5 + await val6 + await val7
console.log(`val5+val6+val7=${val8}`) // val5+val6+val7=6
console.log(`${Date.now() - t2}ms`)   // 500ms

 // 500ms <= 500msの処理3つが、並列で実行された
以下のように、Promise.allを用いることで実現できます。
// 並列にawaitを処理する
const t3 = Date.now()
const val9 = sleep(500, 1)
const val10 = sleep(500, 2)
const val11 = sleep(500, 3)

// Promise.allで並列に処理する.
let val12 = await Promise.all([val9, val10, val11])

// 戻り値は、配列で返ってくる.
console.log('val12:', val12) // val12: [ 1, 2, 3 ]

// 値を合計して
val12 = val12.reduce((sum, val) => sum + val, 0)

// 表示する
console.log(`val9+val10+val11=${val12}`) // val9+val10+val11=6
console.log(`${Date.now() - t3}ms`)      // 500ms

// 500ms <= 500msの処理3つが、並列で実行された
このように並列でPromiseを処理することもできます。Webからリソースを取得する場合などに、並列処理ができると素敵ですね。



エラーを処理する

プロダクションで用いる場合には、エラー処理もちゃんと行う必要があります。ここではいくつかのユースケースを元に、どのようにエラーをハンドリングするかを説明します。


async関数内でエラーが発生する場合

まずは、async関数内でエラーが発生する場合です。例えば以下のように例外が送出されるとします。
async function main2 () {
    throw new Error('MAIN2 ERROR!!!')
}
このエラーをハンドリングするには、catchメソッドで対応します(Promiseのエラー処理と同じですね)。
// catchを用いてエラーを処理する.
main2().catch(err => {
    console.log('main2:err:', err)
})

// main2:err: Error: MAIN2 ERROR!!!
//     at main2 (/Users/munesadayohei/git/frontend-playground/020-async-await/sample.js:64:11)
//     at Object.<anonymous> (/Users/munesadayohei/git/frontend-playground/020-async-await/sample.js:59:1)
//     at Module._compile (module.js:569:30)
//     at Object.Module._extensions..js (module.js:580:10)
//     at Module.load (module.js:503:32)
//     at tryModuleLoad (module.js:466:12)
//     at Function.Module._load (module.js:458:3)
//     at Function.Module.runMain (module.js:605:10)
//     at startup (bootstrap_node.js:158:16)
//     at bootstrap_node.js:575:3
このように、async関数内でのエラーは、Promiseベースで処理していきます。


await対象の処理内でエラーが発生する場合

続いて、await対象のPromiseでエラーが発生する場合です。この場合はawaitで対象を同期的に処理しているため、Promise風にエラー処理できません。代わりにtry/catch構文でエラーをハンドリングします。
// エラーを送出する関数
function rejectFunction () {
    return new Promise((resolve, reject) => {
        throw new Error('REJECT FUNCTION ERROR!!!')

        // rejectした場合にも、同様の扱いになります.
        // reject(new Error('REJECT FUNCTION ERROR!!!'))
    })
}

async function main3 () {
    try {
        await rejectFunction()
    } catch (e) {
        console.log('catched: ', e)
    }
    return 'main3:result: finish'
}
// catched:  Error: REJECT FUNCTION ERROR!!!
//     at Promise (/Users/munesadayohei/git/frontend-playground/020-async-await/sample.js:98:15)
//     at Promise (<anonymous>)
//     at rejectFunction (/Users/munesadayohei/git/frontend-playground/020-async-await/sample.js:97:12)
//     at main3 (/Users/munesadayohei/git/frontend-playground/020-async-await/sample.js:120:15)
//     at Object.<anonymous> (/Users/munesadayohei/git/frontend-playground/020-async-await/sample.js:114:1)
//     at Module._compile (module.js:569:30)
//     at Object.Module._extensions..js (module.js:580:10)
//     at Module.load (module.js:503:32)
//     at tryModuleLoad (module.js:466:12)
//     at Function.Module._load (module.js:458:3)

// main3:result: finish
このように、try/catch構文でエラーハンドリングができるようになります。コールバックベースやPromiseベースよりもシンプルなので良いなと思います。



参考資料

async/awaitを学ぶために、以下の資料を参照しました。ありがとうございます。

- ES async/awaitを全力で使ってみて発見したイディオム - Qiita

- async function - JavaScript | MDN

- Understanding JavaScript’s async await

- ES.next async/await + Promise で解決できる事

- Promiseとasync-awaitの例外処理を完全に理解しよう - Qiita



最後に

async/await構文は便利ですね。やろうやろうと思って数ヶ月放置してましたが、早くやれば良かった。ドシドシと使っていきたいと思います。あと、久しぶりのフロントエンドの記事楽しかった〜。今後も色々とアウトプットしていきたいと思います。

本ブログでは、フロントエンド、Go言語、Python、Linux、Node.js、インフラ、Swift、Java、機械学習、などの技術トピックを発信をしていきます。「プログラミングで困ったその時に、解決の糸口を見つけられる」そんな目標でブログを書き続けています。今後も役立つネタを書いていきますので、ぜひ本ブログのRSSTwitterをフォローして貰えたら嬉しいです ^ ^

最後までご覧頂きましてありがとうございました!





こんな記事もいかがですか?

[取り組み] フロントエンドでコーディングスピードをアップさせる6つの方法!と思って書いてたら30個も書いちゃった。
[フロントエンド] フロントエンドの入社試験99問!難しいですよ〜w。
[フロントエンド] Webページを表示するテストの際に、通信速度を3Gに制限して表示してみよう
[フロントエンド] スマホ実機でのデバッグ手段を増やす!Macのプロキシを利用して、通信内容を確認する。
[フロントエンド] Chrome 35 Beta の変更点。Touch制御、新しいJavaScript機能、プレフィックスなしのShadowDOM
[フロントエンド]複数アカウントでのテストには、Chromeのユーザー管理を使って、Cookieを切り替えると便利
[フロントエンド] Chrome36βが出た。変更点など。element.animate、HTML Imports、Object.observe、他。
RSS画像

もしご興味をお持ち頂けましたら、ぜひRSSへの登録をお願い致します。