去除 try/catch,實作簡潔的 Async 和 Await!

JavaScript

Async 和 Await 提供一種語法糖來撰寫非同步程式碼而看起來很像是同步的樣子,而在這之前若想實作非同步都是使用 callback 或 promise 的方式,產生的問題不外乎是難以閱讀的 callback hell。但,用了 Async 和 Await 就真的能讓程式碼更精簡流暢嗎? ( •́ _ •̀)?

來看一個實際範例,如下所示,doThings 裡面會依序做一些事情…

async function doThings(id) {
  let data = null;

  try {
      await checkDataSourceExist(id);
      data = await fetchDataSource(id);
      renderGraph();
  } catch (error) {
    console.log(`something goes wrong, need to get data from another source: ${error}`);

    try {
      data = await fetchAnotherDataSource(id);
    } catch (err) {
      console.log(`still something goes wrong, throw error to show page: ${error}`);
      displayErrorPage(err);
      throw error;
    }
    throw error;
  }

  return data;
}

承上,得到執行結果

"something goes wrong, need to get data from another source: data not exist"
"still something goes wrong, throw error to show page: data not exist"

原始碼與 Demo

到這裡,發現幾個問題…

有什麼解法嗎?

解法

Cleaner async JavaScript code without the try/catch mess」這篇文章的作者提出一個解法,將 promise 重新包裝,然後將 error 與 data 用陣列 [error, data] 丟出,這樣在回傳的這陣列中,error 與 data 擇一有值,對稍後的邏輯和錯誤處理方便很多。

safeAwait 的原始碼解說如下,分為兩部份。第一部份是將已定義好的 native error 定義在陣列 nativeExceptions 裡面,稍等會將傳入的錯誤依次做檢查。

const nativeExceptions = [
  EvalError,
  RangeError,
  ReferenceError,
  SyntaxError,
  TypeError,
  URIError,
].filter((except) => typeof except === 'function')

檢查傳入的 error 是不是已定義好的 native error,如果是已定義好的 native error 就丟出錯誤。想知道有哪些 native error,可參考這裡

function throwNative(error) {
  for (const Exception of nativeExceptions) {
    if (error instanceof Exception) throw error;
  }
}

關於 for...of 迴圈將可迭代的物件迭代其中元素,可參考這裡

第二部份是用來包裝 promise 的 safeAwait,safeAwait 會做以下幾件事情

function safeAwait(promise, finallyFunc) {
  return promise
    .then(data => {
      if (data instanceof Error) {
        throwNative(data);
        return [data];
      }
      return [undefined, data];
    })
    .catch(error => {
      throwNative(error);
      return [error];
    })
    .finally(() => {
      if (finallyFunc && typeof finallyFunc === 'function') {
        finallyFunc();
      }
    });
}

使用範例如下,這裡 detectALotOfThings 會利用 safeAwait 包裝執行兩個非同步函式 detectSomethingA 和 detectSomethingB,第一個 safeAwait 還加上 callback sayHi。若有 error 則印出 error,沒有 error 就印出 data。

async function detectALotOfThings() {
  const [errorA, dataA] = await safeAwait(detectSomethingA(), sayHi);
  const [errorB, dataB] = await safeAwait(detectSomethingB());

  errorA && console.log(errorA);
  errorB && console.log(errorB);
  dataA && console.log(`data:`, dataA);
  dataB && console.log(`data:`, dataB);
}

const sayHi = () => console.log('Hi');

在這裡假設 detectSomethingA 會丟出「Error A」,detectSomethingB 會丟出「Success B!」。

function detectSomethingA() {
  return new Promise((resolve, reject) =>
    setTimeout(() => reject('Error A'), 100)
  );
}

function detectSomethingB() {
  return new Promise((resolve, reject) =>
    setTimeout(() => resolve('Success B!'), 100)
  );
}

因此會印出結果如下所示。

Hi
error: Error A
data: Success B!

原始碼與 Demo

「Hi」最早被丟出來不是因為 sayHi 最早被執行(執行順序是 return error A -> Hi -> return data Success B),而是因為我們最後才印出執行結果的 error 與 data,而 sayHi 當然早早就跑完了。


不過,實際上我們當然不太可能像這樣 error && console.log(error)data && console.log(data) 的方式撰寫,將本文一開始的例子改寫如下,會比較符合現實的狀況…是不是更簡潔易懂了呢? ヽ(∀゚ )人(゚∀゚)人( ゚∀)人(∀゚ )人(゚∀゚)人( ゚∀)ノ

async function doThings(id) {
  await safeAwait(checkDataSourceExist());
  const [fetchDataErr, fetchDataResult] = await fetchDataSource(detectSomethingB());

  if (fetchDataErr) {
    console.log(`something goes wrong, need to get data from another source: ${fetchDataErr}`);
    const [err, data] = await fetchAnotherDataSource(id);

    if (err) {
      console.log(`still something goes wrong, throw error to show page: ${err}`);
      displayErrorPage(err);
    } else {
      return data;
    }
  } else {
    renderGraph();
    return fetchDataResult;
  }

  return null;
}

參考資料

關於這塊我在下面描述正確的範例。

在 promise 的 then 裡面有錯誤時

若 promise 得到 reject 時,錯誤會由 promise 的 catch 接住;在 promise 的 then 裡面有錯誤時,例如:呼叫某個不存在的 function,則這個錯誤同樣要由 promise 的 catch 接住。因此,以上兩種狀況可以在同一地方統一做 error handling。

與 promise 同層有錯誤時

在 promise 外有錯誤時,也就是同層的時候,需要統一做錯誤處理就必須使用兩個 catch 來分別處理,無法統一在同一個地方。如下範例所示,同步 document.getElementByID 與非同步 fetchDataSource 必須要分開做錯誤處理

function doThings(id) {
  try {
    fetchDataSource()
      .then((data) => {
        console.log(data);

      })
      .catch((e) => {
        console.log('in the inner catch')
        console.log(e.message); // 處理 fetchDataSource 的錯誤
     });
     document.getElementByID('foo'); // 應為 getElementById
  } catch(e) {
    console.log('in the outer catch');
    console.log(e.message);  // 處理 document.getElementByID 的錯誤
  }
}

原始碼與 Demo

以上狀況,可改用 Async / Await 來處理,這樣就能藉由 try…catch 統一處理同步與非同步的錯誤。

async function doThings(id) {
  try {
    const result = await fetchDataSource()
    document.getElementByID('foo'); // 應為 getElementById
  } catch(e) {
    console.log(e.message);  // "should be getElementById" 或 "data is not here"
  }
}

原始碼與 Demo


promise async await ES6 javascript