1. 程式人生 > >如何優雅地處理Async/Await的異常?

如何優雅地處理Async/Await的異常?

譯者按: 使用.catch()來捕獲所有的異常

本文采用意譯,版權歸原作者所有

async/await 中的異常處理很讓人混亂。儘管有很多種方式來應對async 函式的異常,但是連經驗豐富的開發者有時候也會搞錯。

假設你有一個叫做run()的非同步函式。在本文中,我會描述 3 種方式來處理run()的異常情形: try/catch, Go 語言風格, 函式呼叫的時候使用 catch()(即run().catch())。 我會跟你解釋為什麼其實幾乎只需要catch()就足夠。

try/catch

當你第一次使用async/await, 你可能嘗試使用try/catch將每一個 async 操作包圍起來。如果你await一個被 reject 的 Promise,JavaScript 會丟擲一個可以被捕獲的錯誤。

run();

async function run() {
    try {
        await Promise.reject(new Error("Oops!"));
    } catch (error) {
        error.message; // "Oops!"
    }
}

try/catch 能夠捕獲非非同步的異常。

run();

async function run() {
    const v = null;
    try {
        await Promise.resolve("foo");
        v.thisWillThrow;
    } catch (error) {
        // "TypeError: Cannot read property 'thisWillThrow' of null"
        error.message;
    }
}

所以,只需要將所有的程式碼邏輯都用 try/catch包圍起來就可以搞定?也不完全正確。下面的程式碼會丟擲

unhandled promise rejection. await將一個被拒絕的 promise 轉換為可捕獲的錯誤,但是 return 不行。

run();

async function run() {
    try {
        // 注意這裡是return,不是await
        return Promise.reject(new Error("Oops!"));
    } catch (error) {
        // 程式碼不會執行到這裡
    }
}

也不可能使用 return await來繞開。

還有一個缺點就是使用了try/catch 之後,就很難用.的語法來進行 Promise 鏈式組合了。

使用 Go 的語法

另一個常見的方式就是使用then()將一個本來需要用catch()來捕獲並處理的 Promise 轉換為普通的 Promise。然後像 Go 語言中一樣,使用if(err)來處理異常。

run();

async function throwAnError() {
    throw new Error("Oops!");
}

async function noError() {
    return 42;
}

async function run() {
    // The `.then(() => null, err => err)` 來匹配正常/異常的情況。如果正常情況,返回`null`;如果異常,返回`err`
    let err = await throwAnError().then(() => null, err => err);
    if (err != null) {
        err.message; // 'Oops'
    }

    err = await noError().then(() => null, err => err);
    err; // null
}

如果你真的想要同時返回 error 和正確的值,你可以完全假裝在用 Go 語言。

run();

async function throwAnError() {
    throw new Error("Oops!");
}

async function noError() {
    return 42;
}

async function run() {
    // The `.then(v => [null, v], err => [err, null])` pattern
    // 你可以使用陣列解構來匹配err和返回值
    let [err, res] = await throwAnError().then(
        v => [null, v],
        err => [err, null]
    );
    if (err != null) {
        err.message; // 'Oops'
    }

    err = await noError().then(v => [null, v], err => [err, null]);
    err; // null
    res; // 42
}

使用 Go 語言風格的錯誤處理並不能擺脫return無法捕獲的情況。而且還讓整個程式碼更加的複雜,如果忘記if(err != null),就會出問題。

總的來說,有兩大缺點:

  1. 程式碼極度重複,每一個地方都少不了if (err != null) ,真的很累,而且容易漏掉;
  2. run()函式中的非非同步的錯誤也無法處理;

總的來說,它並沒有比try/catch好多少。

在函式呼叫的時候使用catch()

try/catch 和 Go 語言風格的異常處理都有各自的使用場景,但是處理所有異常最好的方法是在run()函式的後面使用catch(),像這樣:run().catch()。換句話說,用一個catch()來處理run函式中的所有錯誤,而不是針對run裡面的每一種情況都去寫程式碼做相應的處理。

run()
    .catch(function handleError(err) {
        err.message; // Oops!
    })
    // 在handleError中處理所有的異常
    // 如果handleError出錯,則退出。
    .catch(err => {
        process.nextTick(() => {
            throw err;
        });
    });

async function run() {
    await Promise.reject(new Error("Oops!"));
}

記住,async 函式總是返回 promise。只要函式中有異常,Promise 會 reject。而且,如果一個 async 函式返回的是一個 reject 的 Promise,那麼這個 Promise 依然會繼續被 reject。

run()
    .catch(function handleError(err) {
        err.message; // Oops!
    })
    .catch(err => {
        process.nextTick(() => {
            throw err;
        });
    });

async function run() {
    // 注意:這裡使用了return,而不是await
    return Promise.reject(new Error("Oops!"));
}

為什麼使用run().catch()而不是將整個run()函式用try/catch包起來呢?我們首先來考慮一個情況:如果try/catchcatch部分有異常,我們應該如何處理呢?只有一個方法:在catch裡面接著使用try/catch。所以,run().catch()的模式使得異常處理變得非常簡潔。

總結

我們最好是全域性的有一個 errorHandler 來處理那些沒有考慮到的異常,比如使用run().catch(handleError),而不是在run()函式裡面所有可能出錯的地方加上try/catch

關於Fundebug

Fundebug專注於JavaScript、微信小程式、微信小遊戲、支付寶小程式、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了10億+錯誤事件,付費客戶有陽光保險、核桃程式設計、荔枝FM、掌門1對1、微脈、青團社等眾多品牌企業。歡迎大家免費試用!

版權宣告

轉載時請註明作者Fundebug以及本文地址:https://blog.fundebug.com/2019/07/24/async-await-erro