0

I am having a bit of trouble flattening out my promises as an alternative to nesting.

Suppose I want to make a table join by retrieving a user and using its id to retrieve associated posts in another table. Like so.

User.get(uid)
.then(usr => {

   return Post.get(usr.id)
})
.then(posts => {
   //send posts or continue the promise chain
});

Now the issue arises whenever I want to execute error handling logic that is specific to a certain promise and not execute anything else. In my example if the DB doesn't find a user with that ID it throws an error, and then if it doesn't find posts by the given foreign key it will also throw an error. What I would like to do is to respond with an error specific message like 'user was not found' or 'posts were not found given the user', etc.

I tried to do this

User.get(uid)
.catch(e => {
   //This executes whenever a user is not found but the problem is that it also 
   //executes the following 'then' statement
})
.then(usr => {

   return Post.get(usr.id)
})
.then(posts => {
   //send posts or continue the promise chain
})
.catch(e => {
   //This should only execute when posts aren't found with the id
});

Now the previous code did not work since the .then executes regardless of an error.

So I thought about removing all .then statements after catch statements, like this

User.get(uid)
.then(usr => {

   return Post.get(usr.id)
})
.then(posts => {
   //send posts or continue the promise chain
})
.catch(e => {
   //This executes whenever a user is not found but the problem is that it also 
   //executes the following 'then' statement
})
.catch(e => {
   //This should only execute when posts aren't found with the id
});

But this doesn't work since the first .catch statement always executes.

In a synchronous fashion this would be the way my code would be written

try
{
   var user = getUser(id);


   try
   {
      var posts = getPosts(user.id);
   }
   catch(e)
   {
      //this executes only if posts aren't found
   }

}
catch (e)
{
   //this executes if the error originates from obtaining a user
}
naughty boy
  • 2,089
  • 3
  • 18
  • 28
  • `then` accepts two arguments: `promise.then(onFulfilled, onRejected)`. See [Handling multiple catches in promise chain](http://stackoverflow.com/a/26077868/6445533) –  Sep 11 '16 at 18:52
  • @ftor yea but from what I'v read it is considered a bit of an anti-pattern to do this... But will have to resort to this option if I can't get the correct answer. – naughty boy Sep 11 '16 at 18:56
  • It is, except for branching. –  Sep 11 '16 at 19:06
  • 1
    I'd ask it this way: why does your DB throw an error, if no item matches your request? Why doesn't it just return an empty set? Imagine an SQL-DB behaving like this. And to your question: do you really need to tell these two errors apart? Ain't it enough to return an empty result if there's no user with this id **or** no posts for this user? *I think you're over-complicating your code, and try to fix a problem that is no real problem.* IMO. this code should not need any catch at all. – Thomas Sep 11 '16 at 19:13
  • @thomas well that is the way it works.. Also there are other instances where you would want your code to behave as described. – naughty boy Sep 11 '16 at 21:37

1 Answers1

0

The heart of the question is :

What I would like to do is to respond with an error specific message like 'user was not found' or 'posts were not found given the user', etc.

Two things :

  • Error objects can be augmented with additional properties.
  • you are not confined to catching - you can also throw/rethrow.

Armed with that, you can approach the problem in (at least) a couple of different ways :

You could write a flat chain :

User.get(uid)
.catch(e => { // initial catch is simple
    e.userMessage = "problem finding user"; // add user message as a custom property 
    throw e; // rethrow the augmented e to ensure the rest of the success path is not executed
})
.then(usr => Post.get(usr.id))
.catch((e) => { // intermediate catch(es) follow this pattern
    if(!e.userMessage) {
        e.userMessage = "problem finding posts"; // add user message as a custom property 
    }
    throw e; // rethrow the augmented e to ensure the rest of the success path is not executed
})
.then(posts => send(posts))
.catch(e => { // terminal catch is a bit different
    console.log(e); // (optional) 
    if(e.userMessage) {
        display(e.userMessage);
    } else {
        display("problem sending pots"); 
    }
});

But the need to distinguish at each stage between latest|earlier error, makes this flat pattern slightly cumbersome.

It's simpler, and well within the spirit of promises, not to flatten. Instead, give each stage its own "private" catch. After the first stage, that means a nested catch.

User.get(uid)
.catch(e => {
    e.userMessage = "problem finding user"; // add user message as a custom property
    throw e; // rethrow the augmented e to ensure the rest of the success path is not executed
})
.then(usr => Post.get(usr.id).catch(e => {
    e.userMessage = "problem finding posts"; // add user message as a custom property 
    throw e; // rethrow the augmented e to ensure the rest of the success path is not executed
}))
.then(posts => send(posts).catch(e => {
    e.userMessage = "problem sending pots"; // add user message as a custom property
    throw e; // rethrow the augmented e
}))
.catch(e => {
    console.log(e); // (optional) log the error with its .message and .userMessage
    display(e.userMessage);
});

Either way, a custom user messsage is provided for errors arising from each of the three possible sources.

Roamer-1888
  • 19,138
  • 5
  • 33
  • 44