Xử lý nhiều sản phẩm khai thác trong chuỗi hứa hẹn


125

Tôi vẫn còn khá mới với những lời hứa và hiện đang sử dụng bluebird, tuy nhiên tôi có một kịch bản mà tôi không chắc chắn làm thế nào để đối phó với nó tốt nhất.

Vì vậy, ví dụ tôi có một chuỗi lời hứa trong một ứng dụng thể hiện như vậy:

repository.Query(getAccountByIdQuery)
        .catch(function(error){
            res.status(404).send({ error: "No account found with this Id" });
        })
        .then(convertDocumentToModel)
        .then(verifyOldPassword)
        .catch(function(error) {
            res.status(406).send({ OldPassword: error });
        })
        .then(changePassword)
        .then(function(){
            res.status(200).send();
        })
        .catch(function(error){
            console.log(error);
            res.status(500).send({ error: "Unable to change password" });
        });

Vì vậy, hành vi tôi theo sau là:

  • Đi lấy tài khoản bằng Id
  • Nếu có sự từ chối tại thời điểm này, hãy ném bom và trả lại lỗi
  • Nếu không có lỗi, chuyển đổi tài liệu được trả về mô hình
  • Xác nhận mật khẩu với tài liệu cơ sở dữ liệu
  • Nếu mật khẩu không khớp thì đánh bom và trả về một lỗi khác
  • Nếu không có lỗi thay đổi mật khẩu
  • Sau đó trở lại thành công
  • Nếu có gì sai, hãy trả lại 500

Vì vậy, các sản phẩm khai thác hiện tại dường như không ngăn được chuỗi, và điều đó có ý nghĩa, vì vậy tôi tự hỏi liệu có cách nào để tôi buộc chuỗi dừng lại ở một điểm nhất định dựa trên các lỗi hay không, nếu có cách nào tốt hơn để cấu trúc này để có được một số dạng hành vi phân nhánh, như có một trường hợp if X do Y else Z.

Bất kỳ sự trợ giúp nào đều sẽ là tuyệt vời.


Bạn có thể suy nghĩ lại hoặc trở về sớm?
Pieter21

Câu trả lời:


126

Hành vi này chính xác giống như một cú ném đồng bộ:

try{
    throw new Error();
} catch(e){
    // handle
} 
// this code will run, since you recovered from the error!

Đó là một nửa điểm .catch- để có thể phục hồi từ các lỗi. Có thể mong muốn được thiết lập lại để báo hiệu trạng thái vẫn là một lỗi:

try{
    throw new Error();
} catch(e){
    // handle
    throw e; // or a wrapper over e so we know it wasn't handled
} 
// this code will not run

Tuy nhiên, điều này một mình sẽ không hoạt động trong trường hợp của bạn vì lỗi được xử lý bởi người xử lý sau. Vấn đề thực sự ở đây là các trình xử lý lỗi "XỬ LÝ BẤT CỨ" tổng quát là một thực tiễn xấu nói chung và cực kỳ khó chịu trong các ngôn ngữ lập trình và hệ sinh thái khác. Vì lý do này, Bluebird cung cấp các sản phẩm khai thác và đánh bắt.

Ưu điểm bổ sung là logic kinh doanh của bạn không (và không nên) phải nhận thức được chu kỳ yêu cầu / phản hồi. Không phải là trách nhiệm của truy vấn khi quyết định trạng thái HTTP và lỗi mà khách hàng gặp phải và sau này khi ứng dụng của bạn phát triển, bạn có thể muốn tách logic nghiệp vụ (cách truy vấn DB của bạn và cách xử lý dữ liệu của bạn) khỏi những gì bạn gửi cho khách hàng (mã trạng thái http, văn bản gì và phản hồi gì).

Đây là cách tôi viết mã của bạn.

Đầu tiên, tôi sẽ .Queryném một NoSuchAccountError, tôi phân lớp nó từ Promise.OperationalErrorđó Bluebird đã cung cấp. Nếu bạn không chắc chắn làm thế nào để phân lớp một lỗi, hãy cho tôi biết.

Tôi cũng phân lớp nó thêm AuthenticationErrorvà sau đó làm một cái gì đó như:

function changePassword(queryDataEtc){ 
    return repository.Query(getAccountByIdQuery)
                     .then(convertDocumentToModel)
                     .then(verifyOldPassword)
                     .then(changePassword);
}

Như bạn có thể thấy - nó rất sạch sẽ và bạn có thể đọc văn bản như một hướng dẫn sử dụng về những gì xảy ra trong quy trình. Nó cũng được tách ra khỏi yêu cầu / phản hồi.

Bây giờ, tôi sẽ gọi nó từ trình xử lý tuyến đường như vậy:

 changePassword(params)
 .catch(NoSuchAccountError, function(e){
     res.status(404).send({ error: "No account found with this Id" });
 }).catch(AuthenticationError, function(e){
     res.status(406).send({ OldPassword: error });
 }).error(function(e){ // catches any remaining operational errors
     res.status(500).send({ error: "Unable to change password" });
 }).catch(function(e){
     res.status(500).send({ error: "Unknown internal server error" });
 });

Theo cách này, logic là tất cả ở một nơi và quyết định xử lý lỗi cho khách hàng là tất cả ở một nơi và chúng không lộn xộn nhau.


11
Bạn có thể muốn thêm rằng lý do có một trình .catch(someSpecificError)xử lý trung gian cho một số lỗi cụ thể là nếu bạn muốn bắt một loại lỗi cụ thể (đó là vô hại), xử lý và tiếp tục dòng chảy tiếp theo. Ví dụ, tôi có một số mã khởi động có một chuỗi các việc cần làm. Điều đầu tiên là đọc tệp cấu hình từ đĩa, nhưng nếu tệp cấu hình đó bị lỗi đó là lỗi OK (chương trình được xây dựng mặc định) để tôi có thể xử lý lỗi cụ thể đó và tiếp tục phần còn lại của luồng. Cũng có thể được dọn dẹp tốt hơn để không rời đi cho đến sau này.
jfriend00

1
Tôi nghĩ rằng "Đó là một nửa của .catch - để có thể phục hồi từ các lỗi" đã làm rõ điều đó nhưng cảm ơn vì đã làm rõ thêm đó là một ví dụ tốt.
Benjamin Gruenbaum

1
Nếu bluebird không được sử dụng thì sao? Plain es6 hứa hẹn chỉ có một thông báo lỗi chuỗi được truyền để bắt.
thợ đồng hồ

3
@clocksmith với ES6 hứa hẹn bạn sẽ instanceoftự mình nắm bắt mọi thứ và tự làm chceks.
Benjamin Gruenbaum

1
Đối với những người đang tìm kiếm một tài liệu tham khảo cho các đối tượng Lỗi phân lớp, hãy đọc bluebirdjs.com/docs/api/catch.html#filtered-catch . Bài viết cũng tái tạo khá nhiều câu trả lời bắt được đưa ra ở đây.
mummybot

47

.catchhoạt động giống như try-catchcâu lệnh, có nghĩa là bạn chỉ cần một lần bắt ở cuối:

repository.Query(getAccountByIdQuery)
        .then(convertDocumentToModel)
        .then(verifyOldPassword)
        .then(changePassword)
        .then(function(){
            res.status(200).send();
        })
        .catch(function(error) {
            if (/*see if error is not found error*/) {
                res.status(404).send({ error: "No account found with this Id" });
            } else if (/*see if error is verification error*/) {
                res.status(406).send({ OldPassword: error });
            } else {
                console.log(error);
                res.status(500).send({ error: "Unable to change password" });
            }
        });

1
Vâng tôi biết điều này nhưng tôi không muốn thực hiện một chuỗi lỗi lớn và dường như dễ đọc hơn khi cần nó. Do đó, tất cả bắt được ở cuối, nhưng tôi thích ý tưởng về các lỗi đánh máy vì đó là mô tả nhiều hơn về ý định.
Grofit

8
@Grofit cho những gì đáng giá - đánh bắt được trong Bluebird ý tưởng của Petka (Esailija) bắt đầu với :) Không cần phải thuyết phục anh ta rằng họ là một cách tiếp cận thích hợp hơn ở đây. Tôi nghĩ rằng anh ấy đã không muốn làm bạn bối rối vì nhiều người trong JS không biết về khái niệm này.
Benjamin Gruenbaum

17

Tôi tự hỏi liệu có cách nào để tôi buộc chuỗi dừng lại ở một điểm nhất định dựa trên các lỗi không

Không. Bạn không thể thực sự "kết thúc" một chuỗi, trừ khi bạn ném một ngoại lệ bong bóng cho đến khi kết thúc. Xem câu trả lời của Benjamin Gruenbaum để biết cách làm điều đó.

Một dẫn xuất của mẫu của anh ta sẽ không phải là để phân biệt các loại lỗi, mà sử dụng các lỗi có statusCodebodycác trường có thể được gửi từ một .catchtrình xử lý chung, duy nhất . Tùy thuộc vào cấu trúc ứng dụng của bạn, giải pháp của anh ấy có thể sạch hơn.

hoặc nếu có một cách tốt hơn để cấu trúc điều này để có được một số dạng hành vi phân nhánh

Có, bạn có thể thực hiện phân nhánh với lời hứa . Tuy nhiên, điều này có nghĩa là rời khỏi chuỗi và "quay lại" để lồng - giống như bạn làm trong một câu lệnh if-if hoặc try-Catch lồng nhau:

repository.Query(getAccountByIdQuery)
.then(function(account) {
    return convertDocumentToModel(account)
    .then(verifyOldPassword)
    .then(function(verification) {
        return changePassword(verification)
        .then(function() {
            res.status(200).send();
        })
    }, function(verificationError) {
        res.status(406).send({ OldPassword: error });
    })
}, function(accountError){
    res.status(404).send({ error: "No account found with this Id" });
})
.catch(function(error){
    console.log(error);
    res.status(500).send({ error: "Unable to change password" });
});

5

Tôi đã làm theo cách này:

Bạn để lại đánh bắt của bạn cuối cùng. Và chỉ cần ném một lỗi khi nó xảy ra giữa chuỗi của bạn.

    repository.Query(getAccountByIdQuery)
    .then((resultOfQuery) => convertDocumentToModel(resultOfQuery)) //inside convertDocumentToModel() you check for empty and then throw new Error('no_account')
    .then((model) => verifyOldPassword(model)) //inside convertDocumentToModel() you check for empty and then throw new Error('no_account')        
    .then(changePassword)
    .then(function(){
        res.status(200).send();
    })
    .catch((error) => {
    if (error.name === 'no_account'){
        res.status(404).send({ error: "No account found with this Id" });

    } else  if (error.name === 'wrong_old_password'){
        res.status(406).send({ OldPassword: error });

    } else {
         res.status(500).send({ error: "Unable to change password" });

    }
});

Các chức năng khác của bạn có thể sẽ trông giống như thế này:

function convertDocumentToModel(resultOfQuery) {
    if (!resultOfQuery){
        throw new Error('no_account');
    } else {
    return new Promise(function(resolve) {
        //do stuff then resolve
        resolve(model);
    }                       
}

4

Có lẽ hơi muộn cho bữa tiệc, nhưng có thể làm tổ .catchnhư thể hiện ở đây:

Mạng lưới nhà phát triển Mozilla - Sử dụng lời hứa

Chỉnh sửa: Tôi đã gửi cái này vì nó cung cấp chức năng được yêu cầu nói chung. Tuy nhiên nó không trong trường hợp cụ thể này. Bởi vì như đã giải thích chi tiết bởi những người khác, .catchnên có nghĩa vụ khôi phục lỗi. Bạn có thể không, ví dụ như gửi một phản ứng cho khách hàng trong nhiều .catch callbacks vì .catchkhông có rõ ràng return giải quyết nó bằng undefinedtrong trường hợp đó, khiến tiến trình .thenđể kích hoạt mặc dù chuỗi của bạn không thực sự giải quyết, có khả năng gây ra một sau .catchđể kích hoạt và gửi một phản hồi khác cho khách hàng, gây ra lỗi và có khả năng ném UnhandledPromiseRejectiontheo cách của bạn. Tôi hy vọng câu nói phức tạp này có ý nghĩa với bạn.


1
@AntonMenshov Bạn nói đúng. Tôi mở rộng câu trả lời của mình, giải thích lý do tại sao hành vi mong muốn của anh ta vẫn không thể thực hiện được với việc lồng
denkquer

2

Thay vì .then().catch()...bạn có thể làm .then(resolveFunc, rejectFunc). Chuỗi hứa hẹn này sẽ tốt hơn nếu bạn xử lý mọi việc trên đường đi. Đây là cách tôi sẽ viết lại nó:

repository.Query(getAccountByIdQuery)
    .then(
        convertDocumentToModel,
        () => {
            res.status(404).send({ error: "No account found with this Id" });
            return Promise.reject(null)
        }
    )
    .then(
        verifyOldPassword,
        () => Promise.reject(null)
    )
    .then(
        changePassword,
        (error) => {
            if (error != null) {
                res.status(406).send({ OldPassword: error });
            }
            return Promise.Promise.reject(null);
        }
    )
    .then(
        _ => res.status(200).send(),
        error => {
            if (error != null) {
                console.error(error);
                res.status(500).send({ error: "Unable to change password" });
            }
        }
    );

Lưu ý: Đây if (error != null)là một chút hack để tương tác với lỗi gần đây nhất.


1

Tôi nghĩ rằng câu trả lời của Benjamin Gruenbaum ở trên là giải pháp tốt nhất cho chuỗi logic phức tạp, nhưng đây là giải pháp thay thế cho các tình huống đơn giản hơn. Tôi chỉ sử dụng một errorEncounteredcờ cùng với return Promise.reject()để bỏ qua bất kỳ báo cáo thenhoặc tiếp theo catch. Vì vậy, nó sẽ trông như thế này:

let errorEncountered = false;
someCall({
  /* do stuff */
})
.catch({
  /* handle error from someCall*/
  errorEncountered = true;
  return Promise.reject();
})
.then({
  /* do other stuff */
  /* this is skipped if the preceding catch was triggered, due to Promise.reject */
})
.catch({
  if (errorEncountered) {
    return;
  }
  /* handle error from preceding then, if it was executed */
  /* if the preceding catch was executed, this is skipped due to the errorEncountered flag */
});

Nếu bạn có nhiều hơn hai cặp sau đó / bắt, có lẽ bạn nên sử dụng giải pháp của Benjamin Gruenbaum. Nhưng điều này làm việc cho một thiết lập đơn giản.

Lưu ý rằng trận chung kết catchchỉ có return;chứ không phải return Promise.reject();thenchúng ta không cần phải bỏ qua và nó sẽ được tính là một từ chối Promise chưa được xử lý, điều mà Node không thích. Như đã viết ở trên, trận chung kết catchsẽ trả lại một Lời hứa được giải quyết một cách hòa bình.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.