Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling multiple catches in promise chain

People also ask

How do you catch errors in promise chains?

There are two ways in which you can handle errors in your promise chain, either by passing an error handler to then block or using the catch operator.

How does catch work in promises?

catch " around the executor automatically catches the error and turns it into rejected promise. This happens not only in the executor function, but in its handlers as well. If we throw inside a . then handler, that means a rejected promise, so the control jumps to the nearest error handler.

Can promise be chained?

Introduction to the JavaScript promise chainingThe callback passed to the then() method executes once the promise is resolved. In the callback, we show the result of the promise and return a new value multiplied by two ( result*2 ).


This behavior is exactly like a synchronous throw:

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

That's half of the point of .catch - to be able to recover from errors. It might be desirable to rethrow to signal the state is still an error:

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

However, this alone won't work in your case since the error be caught by a later handler. The real issue here is that generalized "HANDLE ANYTHING" error handlers are a bad practice in general and are extremely frowned upon in other programming languages and ecosystems. For this reason Bluebird offers typed and predicate catches.

The added advantage is that your business logic does not (and shouldn't) have to be aware of the request/response cycle at all. It is not the query's responsibility to decide which HTTP status and error the client gets and later as your app grows you might want to separate the business logic (how to query your DB and how to process your data) from what you send to the client (what http status code, what text and what response).

Here is how I'd write your code.

First, I'd get .Query to throw a NoSuchAccountError, I'd subclass it from Promise.OperationalError which Bluebird already provides. If you're unsure how to subclass an error let me know.

I'd additionally subclass it for AuthenticationError and then do something like:

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

As you can see - it's very clean and you can read the text like an instruction manual of what happens in the process. It is also separated from the request/response.

Now, I'd call it from the route handler as such:

 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" });
 });

This way, the logic is all in one place and the decision of how to handle errors to the client is all in one place and they don't clutter eachother.


.catch works like the try-catch statement, which means you only need one catch at the end:

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" });
            }
        });

I am wondering if there is a way for me to somehow force the chain to stop at a certain point based upon the errors

No. You cannot really "end" a chain, unless you throw an exception that bubbles until its end. See Benjamin Gruenbaum's answer for how to do that.

A derivation of his pattern would be not to distinguish error types, but use errors that have statusCode and body fields which can be sent from a single, generic .catch handler. Depending on your application structure, his solution might be cleaner though.

or if there is a better way to structure this to get some form of branching behaviour

Yes, you can do branching with promises. However, this means to leave the chain and "go back" to nesting - just like you'd do in an nested if-else or try-catch statement:

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" });
});

I have been doing this way:

You leave your catch in the end. And just throw an error when it happens midway your chain.

    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" });

    }
});

Your other functions would probably look something like this:

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

Probably a little late to the party, but it is possible to nest .catch as shown here:

Mozilla Developer Network - Using Promises

Edit: I submitted this because it provides the asked functionality in general. However it doesn't in this particular case. Because as explained in detail by others already, .catch is supposed to recover the error. You can't, for example, send a response to the client in multiple .catch callbacks because a .catch with no explicit return resolves it with undefined in that case, causing proceeding .then to trigger even though your chain is not really resolved, potentially causing a following .catch to trigger and sending another response to the client, causing an error and likely throwing an UnhandledPromiseRejection your way. I hope this convoluted sentence made some sense to you.


Instead of .then().catch()... you can do .then(resolveFunc, rejectFunc). This promise chain would be better if you handled things along the way. Here is how I would rewrite it:

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" });
            }
        }
    );

Note: The if (error != null) is a bit of a hack to interact with the most recent error.