[js] 非同期処理のdone, failを卒業する


非同期処理を普段どう書いていますか?
僕はdone-failを用いて書いています。実はこの書き方保守性の面であまり良くない。
*jqueryを使用

$.ajax({
   url: 'https://zipcloud.ibsnet.co.jp/api/search?zipcode=7830060'
 })
 .done(function(res) { 
   console.log(res);
 })
 .fail(function(res){ 
   console.log(res);
 });
// =>
// {
//   'message': null,
//   'results': [{ ... }],
//   'status': 200
// }

*以下ajaxの中は簡略化します。

問題1. errorキャッチができない

doneの中でエラーが出たときにfailがキャッチしそうですが実はキャッチできません。

もしdoneの中でエラーハンドリングを行いたい場合はtry-catchを毎回書く必要があります。
さらにtry-catchでネストが深くなるのも嬉しくない。

$.ajax()
 .done(function(res) { 
   throw 'error';
   console.log(res);
 })
 .fail(function(e) { 
   // キャッチできない
   console.log(e);
 });

// Uncaught error 

問題2. コールバック地獄

例えば「ajax通信を複数使いたいが同期的に行いたい時」や「スコープ外の変数へのアクセスのためにコールバックを用いたい時」にネストを深くするスパイラルから抜け出せなくなります。

所謂コールバック地獄。

function successFunc(data) {
  // ...
}

function fetch(callback) {
  $.ajax()
  .done(function(res1) { 
    $.ajax()
    .done(function(res2) { 
      // 終了時の処理
      callback(res2);
     })
    .fail(function(res) { ... }) 
   })
   .fail(function(res) { ... })
})

fetch(successFunc)

解決策

コールバックを防ぐ手段としてPromiseで制御する方法があります。
Promiseに続くthenは同期的に処理を行うことを示しています。したがってコールバック地獄から抜け出せる。また、then-catchを用いることでエラーハンドリングも可能。

少し今風の書き方に変えてこんな感じで書ける。

const fetch = () => {
  return new Promise((resolve, reject) => {
    $.ajax()
    .done(res => {
      // ...
      resolve(res)
    })
    .fail(res => reject(res));
  });
}

// then-catch
fetch().then(res => ... ).catch(e => )

さらにjQueryの$.ajaxはPromiseを内包したDeferredオブジェクトが返ってくるので、より簡潔に書ける。
参考:爆速でわかるjQuery.Deferred超入門

const fetch = () => {
  return $.ajax();
}

fetch().then(res => {
    // ...
    return var
  }).then(var => {
    // ...
    throw 'error'
  }).catch(e => console.log(e))
// => error

ここで注目すべきところはdone-failは連結できないがthenは連結できるというところ。

理由はthenがPromiseオブジェクトを返すから。
参考:Promiseチェーン

async/await

さてPromiseオブジェクトを返しthen-catchを使うことでdone-failを卒業できそうな気がします。

ただ毎回resolve,rejectを書くのは忘れてしまいそうですしthenの見通しが少し悪い気もします。

もっと直感的な書き方はできないのか。。。

ありました!
その名もasync-await

asyncはPromiseを返し、resolveやrejectを明示的に使わずthenチェーンがかけます。
また、asyncの中でawaitをつけた関数がある場合、awaitの関数が終了するまで後続の処理は実行されません。

const fetch = () => {
  return $.ajax(...)
}

const timer2Seconds = res => {
  return new Promise(resolve => {
    setTimeout( () => {
      console.log(res);
      resolve('timeout');
    }, 2000);
  });
}

const main = async () => {
  const res = await fetch();
  const timer = await timer2Seconds(res);
  return timer
}

main().then(message => {throw message}).catch(e => console.log(e));
// => timeout

メリットはこちらの記事がわかりやすいです。
JavaScriptのasync/awaitがPromiseよりもっと良い