Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ExpressJS / NodeJS / Promises: Return early from promise chain

When I get a post request on the server to create a new game, I perform a couple of queries. First, I search if the user is already in a game, returning the game if so. Otherwise, I search for an open game in which someone is waiting for an opponent and return that game if so. Finally, if no game of the above states are found, I create a new game and return that. So my code looks something like this:

.post( function(req, res, next){

  ...findUsersExistingGame...

  .then(function(game){
    if(game){ return res.send(game); }
    else{
        return ...findUserWaitingForOpponentsGame...
    }
  }
  .then(function(game){
    if(game){ return res.send(game); }
    else{
        return ...createNewGame...
    }
  })
  .then(function(game){
        return res.send(game);
  })
  .catch(function(err){
       return next(err);
  });

I will eventually refactor each function into helper functions for better readability, but I need to figure out the chaining first. My question is, if I find a game early in the promise chain (i.e. there's either a user's existing game or another user who is waiting for an opponent) then I return res.send(game); However, the third .then will throw an error because my previous .then() statement returned undefined. How do I return early out of the promise chain if I want to do a res.send(game)?

Option 1: I've seen proposals to throw an error and catch it explicitly, but that feels fundamentally wrong, using errors to control flow that is.

Option 2: Instead of chaining promises, I could do something like this, but this resembles "promise / callback hell":

.post( function(req, res, next){

  ...findUsersExistingGame...

  .then(function(game){
    if(game){ return res.send(game); }
    else{
        ...findUserWaitingForOpponentsGame...
        .then(function(game){
            if(game){ return res.send(game); }
            else{
                return ...createNewGame...
                .then(function(game){
                    return res.send(game);
                });
            }
        })
    }
  }

Is there another way (preferably in ES5 since I'm still trying to understand promises fundamentally, but ES6 answers welcome as well)?

like image 852
PDN Avatar asked Mar 13 '23 09:03

PDN


1 Answers

The main issue here is that you have three possible return values from each step along the way:

  1. Game found
  2. Game not found yet
  3. Error while looking for game

Since promises only naturally separate error and no error, as long as you want to handle each of those three separate returns differently, you're going to have add some of your own branching logic.

To do branching cleanly with promise results requires additional levels of nesting and there is usually no reason to avoid it as it will make your code the easiest to follow and understand the logic for.

.post( function(req, res, next) {
    findUsersExistingGame(...).then(function(game) {
        if (game) return game;
        return findUserWaitingForOpponentsGame(...).then(function(game) {
            if (game) return game;
            // createNewGame() either resolves with a valid game or rejects with an error
            return createNewGame(...);
        });
    }).then(function(game) {
        res.send(game);
    }, function(err) {
        // send an error response here
    });
});

Note how this has simplified the return at each stage, and it returns the next nested promise to make things chain and it has centralized the handling of sending the response to one place to reduce the overall code.


Now, you could hide some of this logic by having each of your functions accept the previous game value and have them check to see if there's already a valid game and if so, they do nothing:

.post( function(req, res, next) {
    findUsersExistingGame(args)
        .then(findUserWaitingForOpponentsGame)
        .then(createNewGame)
        .then(function(game) {
            res.send(game);
        }, function(err) {
            // send an error response here
        });
});

But, inside of findUserWaitingForOpponentsGame(), you'd have to accept the exact arguments that findUsersExistingGame() resolved to and you'd have to check if the game was valid or not.

function findUserWaitingForOpponentsGame(args) {
    if (args.game) {
        return Promise.resolve(args);
    } else {
        return doAsyncFindUserWaitingForOpponentsGame(args);
    }
}

Each function would resolve with the args object that had any common parameters on it and had the .game property which each level could check. While this gives you a nice clean control flow, it does create extra code in each function and it forces each function to accept arguments that are the output of the previous function (so you can have straight chaining). You can decide which you like better.

like image 164
jfriend00 Avatar answered Apr 26 '23 09:04

jfriend00