簡體   English   中英

異常處理,拋出錯誤,在承諾內

[英]exception handling, thrown errors, within promises

我正在運行外部代碼作為 node.js 服務的第 3 方擴展。 API 方法返回承諾。 已解決的承諾意味着操作成功執行,失敗的承諾意味着執行操作存在問題。

現在這就是我遇到麻煩的地方。

由於第 3 方代碼未知,因此可能存在錯誤、語法錯誤、類型問題以及任何可能導致 node.js 拋出異常的事情。

但是,由於所有代碼都包含在 Promise 中,因此這些拋出的異常實際上是作為失敗的 Promise 返回的。

我試圖將函數調用放在 try/catch 塊中,但從未觸發過:

// worker process
var mod = require('./3rdparty/module.js');
try {
  mod.run().then(function (data) {
    sendToClient(true, data);
  }, function (err) {
    sendToClient(false, err);
  });
} catch (e) {
  // unrecoverable error inside of module
  // ... send signal to restart this worker process ...
});

在上面的偽代碼示例中,當拋出錯誤時,它會出現在失敗的承諾函數中,而不是在 catch 中。

從我讀到的,這是一個功能,而不是一個問題,有承諾。 但是,我無法理解為什么您總是希望完全相同地對待異常和預期拒絕。

一種情況是關於代碼中的實際錯誤,可能無法恢復——另一種情況可能是缺少配置信息、參數或可恢復的東西。

感謝您的幫助!

崩潰和重新啟動進程不是處理錯誤的有效策略,甚至不是錯誤。 在 Erlang 中會很好,在那里一個進程很便宜並且只做一件孤立的事情,比如為單個客戶端提供服務。 這不適用於節點,其中一個流程的成本要高出幾個數量級並同時為數千個客戶提供服務

假設您的服務每秒處理 200 個請求。 如果其中有 1% 的人在您的代碼中遇到了問題,那么您將每秒關閉 20 次進程,大約每 50 毫秒一次。 如果您有 4 個內核,每個內核有 1 個進程,那么您將在 200 毫秒內丟失它們。 因此,如果一個進程需要超過 200 毫秒的時間來啟動和准備服務請求(對於不加載任何模塊的節點進程,最低成本約為 50 毫秒),我們現在成功地完全拒絕服務。 更不用說遇到錯誤的用戶往往會做一些事情,例如反復刷新頁面,從而使問題復雜化。

域不能解決問題,因為它們不能確保資源不泄漏

在問題#5114#5149 中閱讀更多內容。

現在您可以嘗試對此“聰明”,並根據一定數量的錯誤制定某種流程回收策略,但無論您采用何種策略,它都會嚴重改變節點的可擴展性配置文件。 我們說的是每個進程每秒有幾十個請求,而不是幾千個。

然而,promise 捕獲所有異常,然后以與同步異常向上傳播堆棧的方式非常相似的方式傳播它們。 此外,他們經常提供一種方法, finally其目的是要成為一個相當於try...finally由於這兩個特點,我們可以通過建立“上下文經理人”(類似於封裝了清理邏輯with蟒蛇usingC# 中或在Java 中try-with-resources ) 總是清理資源。

讓我們假設我們的資源被表示為具有acquiredispose方法的對象,這兩個方法都返回承諾。 調用函數時沒有建立連接,我們只返回一個資源對象。 稍后將using對象處理此對象:

function connect(url) {
  return {acquire: cb => pg.connect(url), dispose: conn => conn.dispose()}
}

我們希望 API 像這樣工作:

using(connect(process.env.DATABASE_URL), async (conn) => {
  await conn.query(...);
  do other things
  return some result;
});

我們可以輕松實現這個 API:

function using(resource, fn) {
  return Promise.resolve()
    .then(() => resource.acquire())
    .then(item => 
      Promise.resolve(item).then(fn).finally(() => 
        // bail if disposing fails, for any reason (sync or async)
        Promise.resolve()
          .then(() => resource.dispose(item))
          .catch(terminate)
      )
    );
}

在 using 的fn參數中返回的承諾鏈完成后,資源將始終被處理。 即使在該函數(例如從JSON.parse )或其內部.then閉包(如第二個JSON.parse )中拋出錯誤,或者如果鏈中的承諾被拒絕(相當於回調調用錯誤)。 這就是為什么承諾捕獲錯誤並傳播它們如此重要的原因。

但是,如果處理資源確實失敗,那確實是終止的好理由。 在這種情況下,我們極有可能泄露了資源,最好開始結束該過程。 但是現在我們崩潰的機會被隔離在我們代碼的一小部分 - 實際上處理可泄漏資源的部分!

注意:terminate 基本上是在帶外拋出,因此 promise 無法捕獲它,例如process.nextTick(() => { throw e }); . 什么實現有意義可能取決於您的設置——基於 nextTick 的實現類似於回調保釋的方式。

如何使用基於回調的庫? 它們可能不安全。 讓我們看一個例子,看看這些錯誤可能來自哪里以及哪些可能導致問題:

function unwrapped(arg1, arg2, done) {
  var resource = allocateResource();
  mayThrowError1();
  resource.doesntThrow(arg1, (err, res) => {
    mayThrowError2(arg2);
    done(err, res);
  });
}

mayThrowError2()是內回調中,將仍然崩潰的過程中,如果它拋出,即使unwrapped的又一承諾的范圍內叫.then 這些類型的錯誤不會被典型的promisify包裝器捕獲,並且會像往常一樣繼續導致進程崩潰。

然而, mayThrowError1()將由如果調用中的承諾被捕獲.then ,和內部分配的資源可能會泄露。

我們可以編寫一個偏執版本的promisify ,以確保任何拋出的錯誤都是不可恢復的,並使進程崩潰:

function paranoidPromisify(fn) {
  return function(...args) {
    return new Promise((resolve, reject) =>   
      try {
        fn(...args, (err, res) => err != null ? reject(err) : resolve(res));
      } catch (e) {
        process.nextTick(() => { throw e; });
      }
    }
  }
}

使用其他承諾中的promisified函數.then回調現在,如果展開拋出,落回重磨式碰撞模式進程崩潰的結果。

人們普遍希望隨着您使用越來越多的基於 Promise 的庫,他們將使用上下文管理器模式來管理他們的資源,因此您不需要讓進程崩潰。

這些解決方案都不是防彈的 - 甚至不會因拋出的錯誤而崩潰。 很容易不小心寫出盡管沒有拋出卻泄漏資源的代碼。 例如,這個節點樣式函數即使沒有拋出也會泄漏資源:

function unwrapped(arg1, arg2, done) {
  var resource = allocateResource();
  resource.doSomething(arg1, function(err, res) {
    if (err) return done(err);
    resource.doSomethingElse(res, function(err, res) {
      resource.dispose();
      done(err, res);
    });
  });
}

為什么? 因為當doSomething的回調收到錯誤時,代碼會忘記處理資源。

上下文管理器不會發生這種問題。 您不能忘記調用 dispose:您不必這樣做,因為using為您服務!

參考資料: 為什么我要切換到 promises上下文管理器和事務

這幾乎是 Promise 最重要的特性。 如果它不存在,您不妨使用回調:

var fs = require("fs");

fs.readFile("myfile.json", function(err, contents) {
    if( err ) {
        console.error("Cannot read file");
    }
    else {
        try {
            var result = JSON.parse(contents);
            console.log(result);
        }
        catch(e) {
            console.error("Invalid json");
        }
    }

});

(在您說JSON.parse是 js 中唯一拋出的東西之前,您是否知道即使將變量強制為數字,例如+a也會拋出TypeError

然而,上面的代碼可以用 promise 更清楚地表達,因為只有一個異常通道而不是 2 個:

var Promise = require("bluebird");
var readFile = Promise.promisify(require("fs").readFile);

readFile("myfile.json").then(JSON.parse).then(function(result){
    console.log(result);
}).catch(SyntaxError, function(e){
    console.error("Invalid json");
}).catch(function(e){
    console.error("Cannot read file");
});

請注意, catch.then(null, fn)糖。 如果您了解異常流的工作原理,您會發現通常使用.then(fnSuccess, fnFail)是一種反模式

重點不是.then(success, fail) over , function(fail, success) (即它不是附加回調的替代方法),而是使編寫的代碼看起來與編寫時看起來幾乎相同同步代碼:

try {
    var result = JSON.parse(readFileSync("myjson.json"));
    console.log(result);
}
catch(SyntaxError e) {
    console.error("Invalid json");
}
catch(Error e) {
    console.error("Cannot read file");
}

(同步代碼實際上會更丑,因為 javascript 沒有輸入捕獲)

Promise 拒絕只是失敗抽象的一種形式。 節點式回調(err、res)和異常也是如此。 由於 Promise 是異步的,因此您不能使用 try-catch 來實際捕獲任何內容,因為錯誤可能不會發生在事件循環的同一個滴答中。

一個簡單的例子:

function test(callback){
    throw 'error';
    callback(null);
}

try {
    test(function () {});
} catch (e) {
    console.log('Caught: ' + e);
}

在這里我們可以捕獲錯誤,因為函數是同步的(雖然基於回調)。 另一個:

function test(callback){
    process.nextTick(function () {
        throw 'error';
        callback(null); 
    });
}

try {
    test(function () {});
} catch (e) {
    console.log('Caught: ' + e);
}

現在我們無法捕捉到錯誤! 唯一的選擇是在回調中傳遞它:

function test(callback){
    process.nextTick(function () {
        callback('error', null); 
    });
}

test(function (err, res) {
    if (err) return console.log('Caught: ' + err);
});

現在它就像第一個例子一樣工作。這同樣適用於承諾:你不能使用 try-catch,所以你使用拒絕來處理錯誤。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM