Search code examples
javascriptfunctional-programmingpromisees6-promise

How to unnest these Promises?


Background

I have a function that makes a request to a server. If the request fails, I want to: 1. log the error 2. run a terminal command 2.1 log if the command failed or succeeded

To achieve this I have the following code:

const createRequest = ( { request, logger, terminal } ) => ( { endpoint, timeout } ) =>
    request.get( endpoint, { timeout } )
        .then( response =>
            logger.info( { event: "Heartbeat request succeeded.", status: response.status } )
        )
        .catch( err =>
            logger.error( { event: "Heartbeat request failed.", err } )
                .then( ( ) => terminal.runAsync( "pm2 restart myAPI" ) )
                .then( ( ) => logger.info( { event: "Restarted API." } ) )
                .catch( err => logger.error( { event: "Failed to restart API.",  err } ) )
        );

Now, there are a few things to notice: - logging is async ( sends info to a remote server ) - running the terminal command is async - making the request is ( obviously ) async

Problem?

The problem I have is that my catch has a Promise inside, which means I have nesting. Now, I am strongly against nesting of promises, so I really want to get rid of that, but I just don't see how.

Question

  1. Is it possible to get rid of nesting promises inside a catch ?
  2. If so how?

Solution

  • Problem?

    The problem I have is that my catch has a Promise inside, which means I have nesting. Now, I am strongly against nesting of promises, so I really want to get rid of that, but I just don't see how.

    – Flame_Phoenix

    The problem is that you think you have a problem – or maybe that you posted this question on StackOverflow instead of CodeReview. The article you linked shows where you adopt a naive view about nested promises

    You get a whole bundle of promises nested in eachother:

    loadSomething().then(function(something) {
        loadAnotherthing().then(function(another) {
                        DoSomethingOnThem(something, another);
        });
    });
    

    The reason you’ve done this is because you need to do something with the results of both promises, so you can’t chain them since the then() is only passed the result of the previous return.

    The real reason you’ve done this is because you don’t know about the Promise.all() method.

    – Code Monkey, http://taoofcode.net

    No, Promise.all can only sometimes replace nested promises. A simple counter-example – here, one promise's value depends on the the other and so the two must be sequenced

    getAuthorByUsername (username) .then (a => getArticlesByAuthorId (a.id))
    

    Nesting promises is not always necessary, but calling it an "anti-pattern" and encouraging people to avoid it before they know the difference is harmful, imo.


    statements are not functional

    The other linked article shows where you may have been misguided again

    Don’t get me wrong, async/await is not the source of all evil in the world. I actually learned to like it after a few months of using it. So, if you feel confortable writing imperative code, learning how to use async/await to manage your asynchronous operations might be a good move.

    But if you like promises and you like to learn and apply more and more functional programming principles to your code, you might want to just skip async/await code entirely, stop thinking imperative and move to this new-old paradigm.

    – Gabriel Montes

    Only this doesn't make any sense. If you look at all of the imperative keywords in JavaScript, you'll notice none of them evaluate to a value. To illustrate what I mean, consider

    let total = if (taxIncluded) { total } else { total + (total * tax) }
    // SyntaxError: expected expression, got keyword 'if'
    

    Or if we try to use if in the middle of another expression

    makeUser (if (person.name.length === 0) { "anonymous" } else { person.name })
    // SyntaxError: expected expression, got keyword 'if'
    

    That's because if is a statement and it never evaluates to a value – instead, it can only rely on side effects.

    if (person.name.length === 0)
      makeUser ("anonymous") // <-- side effect
    else
      makeUser (person.name) // <-- side effect
    

    Below for never evaluates to a value. Instead it relies on side effects to compute sum

    let sum = 0
    let numbers = [ 1, 2, 3 ]
    for (let n of numbers)
      sum = sum + n        // <-- side effect
    console.log (sum)      // 6
    

    The same is true for do, while, switch, even return and all of the other imperative keywords – they're all statements and therefore rely upon side effects to compute values.

    What evaluates to a value then? Expressions evaluate to a value

    1                          // => 1
    5 + 5                      // => 10
    person.name                // => "bobby"
    person.name + person.name  // => "bobbybobby"
    toUpper (person.name)      // => "BOBBY"
    people .map (p => p.name)  // => [ "bobby", "alice" ]
    

    async and await are not statements

    You can assign an asynchronous function to a variable

    const f = async x => ...
    

    Or you can pass an asyncrhonous function as an argument

    someFunc (async x => ... )
    

    Even if an async function returns nothing, async still guarantees we will receive a Promise value

    const f = async () => {}
    f () .then (() => console.log ("done"))
    // "done"
    

    You can await a value and assign it to a variable

    const items = await getItems () // [ ... ]
    

    Or you can await a value in another expression

    items .concat (await getMoreItems ()) // [ ... ]
    

    It's because async/await form expressions that they can be used with functional style. If you are trying to learn functional style and avoid async and await, it is only because you've been misguided. If async and await were imperative style only, things like this would never be possible

    const asyncUnfold = async (f, initState) =>
      f ( async (value, nextState) => [ value, ...await asyncUnfold (f, nextState) ]
        , async () => []
        , initState
        )
    

    real example

    Here's a practical example where we have a database of records and we wish to perform a recursive look-up, or something...

    const data =
      { 0 : [ 1, 2, 3 ]
      , 1 : [ 11, 12, 13 ]
      , 2 : [ 21, 22, 23 ]
      , 3 : [ 31, 32, 33 ]
      , 11 : [ 111, 112, 113 ]
      , 33 : [ 333 ]
      , 333 : [ 3333 ]
      }
    

    An asynchronous function Db.getChildren stands between you and your data. How do you query a node and all of its descendants?

    const Empty =
      Symbol ()
    
    const traverse = (id) =>
      asyncUnfold
        ( async (next, done, [ id = Empty, ...rest ]) =>
            id === Empty
              ? done ()
              : next (id, [ ...await Db.getChildren (id), ...rest ])
        , [ id ]
        )
    
    traverse (0)
    // => Promise [ 0, 1, 11, 111, 112, 113, 12, 13, 2, 21, 22, 23, 3, 31, 32, 33, 333, 3333 ]
    

    A pure program sent from the "heaven of JavaScript developers", to put it in the words of Montes. It's written using a functional expression, errors bubble up accordingly, and we didn't even have to touch .then.

    We could write the same program using imperative style. Or we could write it functional style using .then too. We can write it all sorts of ways and I guess that's the point – Thanks to async and await's ability to form expressions, we can use them in a variety of styles, including functional style.

    Run the entire program in your browser below

    const asyncUnfold = async (f, init) =>
      f ( async (x, acc) => [ x, ...await asyncUnfold (f, acc) ]
        , async () => []
        , init
        )
    
    const Db =
      { getChildren : (id) =>
          new Promise (r => setTimeout (r, 100, data [id] || []))
      }
    
    const Empty =
      Symbol ()
    
    const traverse = (id) =>
      asyncUnfold
        ( async (next, done, [ id = Empty, ...rest ]) =>
            id === Empty
              ? done ()
              : next (id, [ ...await Db.getChildren (id), ...rest ])
        , [ id ]
        )
        
    const data =
      { 0 : [ 1, 2, 3 ]
      , 1 : [ 11, 12, 13 ]
      , 2 : [ 21, 22, 23 ]
      , 3 : [ 31, 32, 33 ]
      , 11 : [ 111, 112, 113 ]
      , 33 : [ 333 ]
      , 333 : [ 3333 ]
      }
    
    traverse (0) .then (console.log, console.error)
    // => Promise
    // ~2 seconds later
    // [ 0, 1, 11, 111, 112, 113, 12, 13, 2, 21, 22, 23, 3, 31, 32, 33, 333, 3333 ]