Handling multiple catches in promise chain
Asked Answered
M

8

157

I am still fairly new to promises and am using bluebird currently, however I have a scenario where I am not quite sure how to best deal with it.

So for example I have a promise chain within an express app like so:

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

So the behaviour I am after is:

  • Goes to get account by Id
  • If there is a rejection at this point, bomb out and return an error
  • If there is no error convert the document returned to a model
  • Verify the password with the database document
  • If the passwords dont match then bomb out and return a different error
  • If there is no error change the passwords
  • Then return success
  • If anything else went wrong, return a 500

So currently catches do not seem to stop the chaining, and that makes sense, so 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, or if there is a better way to structure this to get some form of branching behaviour, as there is a case of if X do Y else Z.

Any help would be great.

Mundy answered 27/9, 2014 at 16:2 Comment(1)
Can you either rethrow or early return?Process
G
142

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.

Gusman answered 27/9, 2014 at 18:6 Comment(10)
You might want to add that the reason for having an intermediate .catch(someSpecificError) handler for some specific error is if you want to catch a specific type of error (that is harmless), deal with it and continue the flow that follows. For example, I have some startup code that has a sequence of things to do. The first thing is to read the config file from disk, but if that config file is misssing that's an OK error (the program has built in defaults) so I can handle that specific error and continue the rest of the flow. There may also be cleanup better to not leave until later.Passel
I thought that "That's half of the point of .catch - to be able to recover from errors" made that clear but thanks for clarifying further that's a good example.Gusman
I have given this one the answer as although the other answers are very similar and solve the problem the same way, this has the most information which can help others on the same subject, but have upvoted all useful ones, thanks again!Mundy
What if bluebird is not being used? Plain es6 promises only have a string error message that is passed to catch.Paediatrics
@Paediatrics with ES6 promises you're stuck catching everything and doing instanceof chceks manually yourself.Gusman
@BenjaminGruenbaum what if you want to throw an error and exit the promise chain (to catch only the first error in case this error would avoid the following functions nolw to work properly, for example when you fetch some data in the DB and use it in the next functions)?Mashe
@Mashe how would that sort of code if it were synchronous?Gusman
For those looking for a reference for subclassing Error objects read bluebirdjs.com/docs/api/catch.html#filtered-catch. Article also pretty much reproduces the multiple catch answer given here.Garboard
@Garboard today, I'd do class MyError extends Error.Gusman
That code there at the end is actually a little beautiful in its neatness and readability.Limiting
U
63

.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" });
            }
        });
Unwholesome answered 27/9, 2014 at 17:59 Comment(4)
Yeah I knew of this but I didn't want to do a huge error chain, and it seemed more readable doing it as and when it needed it. Hence the catch all at the end, but I like the idea of typed errors as that is more descriptive as to the intent.Mundy
@Mundy for what it's worth - typed catches in Bluebird were Petka (Esailija)'s idea to begin with :) No need to convince him they're a preferable approach here. I think he didn't want to confuse you since a lot of people in JS aren't very aware of the concept.Gusman
Do I have to if (!res.ok)throw new Error() in one of the then()?Santana
probably throw .. creates an error.Santana
V
18

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" });
});
Verdi answered 27/9, 2014 at 18:35 Comment(0)
N
9

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);
    }                       
}
Noise answered 16/4, 2018 at 1:49 Comment(0)
V
7

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.

Valeriavalerian answered 10/8, 2019 at 23:58 Comment(1)
@AntonMenshov You are right. I expanded my answer, explaining why his desired behavior is still not possible with nestingValeriavalerian
H
3

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.

Hankhanke answered 30/4, 2018 at 21:43 Comment(0)
L
2

I think Benjamin Gruenbaum's answer above is the best solution for a complex logic sequence, but here is my alternative for simpler situations. I just use an errorEncountered flag along with return Promise.reject() to skip any subsequent then or catch statements. So it would look like this:

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 */
});

If you have more than two then/catch pairs, you should probably use Benjamin Gruenbaum's solution. But this works for a simple set-up.

Note that the final catch only has return; rather than return Promise.reject();, because there's no subsequent then that we need to skip, and it would count as an unhandled Promise rejection, which Node doesn't like. As is written above, the final catch will return a peacefully resolved Promise.

Limiting answered 27/11, 2018 at 19:15 Comment(0)
R
0

I wanted to preserve the branching behaviour that Bergi's answer had, yet still provide the clean code structure of unnested .then()'s

If you can handle some ugliness in the machinery that makes this code work, the result is a clean code structure similar to non-nested chained .then()'s

One nice part of structuring a chain like this, is that you can handle all the potential results in one place by chainRequests(...).then(handleAllPotentialResults) this might be nice if you need to hide the request chain behind some standardised interface.

const log = console.log;
const chainRequest = (stepFunction, step) => (response) => {
    if (response.status === 200) {
        return stepFunction(response, step);
    }
    else {
        log(`Failure at step: ${step}`);
        return response;
    }
};
const chainRequests = (initialRequest, ...steps) => {
    const recurs = (step) => (response) => {
        const incStep = step + 1;
        const nextStep = steps.shift();
        return nextStep ? nextStep(response, step).then(chainRequest(recurs(incStep), incStep)) : response;
    };
    return initialRequest().then(recurs(0));
};
// Usage 
async function workingExample() {
    return await chainRequests(
        () => fetch('https://jsonplaceholder.typicode.com/users'), 
        (resp, step) => { log(`step: ${step}`, resp); return fetch('https://jsonplaceholder.typicode.com/posts/'); },
        (resp, step) => { log(`step: ${step}`, resp); return fetch('https://jsonplaceholder.typicode.com/posts/3'); }
    );
}
async function failureExample() {
    return await chainRequests(
        () => fetch('https://jsonplaceholder.typicode.com/users'),
        (resp, step) => { log(`step: ${step}`, resp); return fetch('https://jsonplaceholder.typicode.com/posts/fail'); },
        (resp, step) => { log(`step: ${step}`, resp); return fetch('https://jsonplaceholder.typicode.com/posts/3'); }
    );
}
console.log(await workingExample());
console.log(await failureExample());

The idea is there, but the interface exposed could probably use some tweaking.

Seeing as this implementation used curried arrow functions, the above could potentially be implemented with more direct async/await code

Renovate answered 12/7, 2021 at 12:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.