Search code examples
javascriptasync-awaites6-promise

"A little help!" with the asynchronous syntax in javascript. I'm an old newbie, and I've finding this more than a little frustrating


I’d just like to run an async function, have it wait till it has a return value, pass that return value to another async function and wash and repeat a few times. I’ve seen some examples but I’m struggling to make them work in test code. Forgive all the baseball context, but I had to have a little fun with this self-inflicted nose bleed.

I can't even make it work without passing return values in the attached code, feeling pretty stupid at the moment...

whosOnFirst(1)
.then(whatsOnSecond(2))
.then(iDunnosOnthird(3)) 

I'd like to see the syntax to have whosOnFirst(1) execute, pass a return value to whatsOnSecond() and pass it's return value to iDunnosOnthird(). Creating the desired output (less the spaces) referenced in the bottom of the snippet.

I really appreciate the help!

// Arbitrary timer with input/output parameters for async understanding;
function waitSec(callerName, pause) { // 
    console.log(`${callerName} called, waiting ${pause} second(s)...`)
    setTimeout(function () {
        console.log(`${callerName} sent ${pause + 1} in a return, and done!`)
        return pause + 1; // delayed return value / adds 1 to input param 
    }, pause * 1000);
}

// Who's On First - run this first with an input param = 1
async function whosOnFirst(i) {
    waitSec("who's on first", 1).then(result => {
        return result;
    })
}

// What's on Second - run after completion of whosOnFirst() using return for param
async function whatsOnSecond(i) {
    waitSec("what's on second", i).then(result => {
        return result;
    })
}

// iDunno's on third - run after completion of whatsOnSecond(i) using return for param
async function iDunnosOnthird(i) {
    waitSec("iDunno's on third", i).then(result => {
        return result;
    })
}

whosOnFirst(1)
    .then(whatsOnSecond(2))
    .then(iDunnosOnthird(3))


// CURRENT OUTPUT: 
// who's on first called, waiting 1 second(s)...
// what's on second called, waiting 2 second(s)...
// iDunno's on third called, waiting 3 second(s)...
// who's on first sent 2 in a return, and done!
// what's on second sent 3 in a return, and done!
// iDunno's on third sent 4 in a return, and done!

// DESIRED OUTPUT:
// who's on first called, waiting 1 second(s)...
// who's on first sent 2 in a return, and done!

// what's on second called, waiting 2 second(s)...
// what's on second sent 3 in a return, and done!

// iDunno's on third called, waiting 3 second(s)...
// iDunno's on third sent 4 in a return, and done!


Solution

  • Working Solution

    Your issue is that the waitSec is in a different context from the rest of the code. You need to lift it to the Promise context.

    Then you have "promisified" code all the way down, and you can chain promises. Here it is working (further explanation below it):

    // Arbitrary timer with input/output parameters for async understanding;
    function waitSec(callerName, pause) { // 
        return new Promise(resolve => {
          console.log(`${callerName} called, waiting ${pause} second(s)...`)
          setTimeout(function () {
            console.log(`${callerName} sent ${pause + 1} in a return, and done!`)
            resolve(pause + 1); // delayed return value / adds 1 to input param 
          }, pause * 1000);
        })
    }
    
    // Who's On First - run this first with an input param = 1
       function whosOnFirst(i) {
       return waitSec("who's on first", 1)
    }
    
    // What's on Second - run after completion of whosOnFirst() using return for param
    function whatsOnSecond(i) {
       return waitSec("what's on second", i)
    }
    
    // iDunno's on third - run after completion of whatsOnSecond(i) using return for param
    function iDunnosOnthird(i) {
        return waitSec("iDunno's on third", i)
    }
    
    whosOnFirst(1)
        .then(whatsOnSecond)
        .then(iDunnosOnthird)
        .then(console.log)

    Further Explanation

    Your original implementation of waitSec always returns undefined to the caller:

    // Arbitrary timer with input/output parameters for async understanding;
    function waitSec(callerName, pause) { // 
        console.log(`${callerName} called, waiting ${pause} second(s)...`)
        setTimeout(function () {
            console.log(`${callerName} sent ${pause + 1} in a return, and done!`)
            return pause + 1; // delayed return value / adds 1 to input param 
        }, pause * 1000);
        // return undefined happens here
    }
    

    The return inside the setTimeout does not return to your calling code, because your code never calls this function. It is invoked by the JS timer code, asynchronously.

    Returning values from async code using a callback

    The way to communicate a result out of there to a caller is to either take a callback as a parameter to the outer function and call that callback in the async function, like this:

    function waitSec({callerName, pause, callback}) {
          console.log(`${callerName} called, waiting ${pause} second(s)...`)
          setTimeout(function () {
            console.log(`${callerName} sent ${pause + 1} in a return, and done!`)
            callback(pause + 1); // delayed return value / adds 1 to input param 
          }, pause * 1000);
    }
    

    In which case you have the classic callback pattern of JS.

    Using Promises, and why you might prefer them

    Or, you return a Promise, and resolve it to return the value - as I demonstrated in the solution. For more about that, search for "how do I promisify a callback".

    The benefit of doing it this way (with a Promise) is that Promises are composable, as demonstrated in the solution.

    Promises are also await-able, so you can use async/await with Promise-returning functions.

    You do not need to mark the Promise-returning functions as async.

    If you mark a function as async, it returns a Promise, like this:

    // A function marked async returns a Promise
    async function simple() {
      return 3
    }
    
    // Proof that it is a Promise 
    simple().then(console.log)
    
    // The equivalent code explicitly returning a Promise - this is what `async` does
    function simpleP(n = 0) {
      return new Promise(resolve => resolve(n+1))
    }
    
    // Proof that it behaves the same
    simpleP().then(console.log)
    
    // Promise composition
    simple()
      .then(simpleP)
      .then(console.log)
      
    // Same functionality, using async/await
    async function main() {
      const num = await simple()
      const res = await simpleP(num)
      console.log(`${num} + 1 = ${res}`)
    }
    
    main()

    Marking synchronous return functions async makes them composable with async code that actually needs a Promise to return a value. You can use it to lift your functions to the same async context and compose them.

    // Synchronous function that is Promisified and composable
    async function simple() {
       return 3
    }
    

    To actually get a value out of an asynchronous function that takes a callback and provides its return via that, you must construct and return a Promise from your function, and then call the Promise resolve inside the callback that provides the value you want to communicate out.

    function getDBRecordP(id) {
      return new Promise((resolve, reject) => 
        dbLib.get(id, 
          (err, result) => err ? 
            reject(err) : 
            resolve(result)))
    }
    

    In this case, you have to explicitly wire up the Promise machinery. This is pretty much the only time that you need to create and return a Promise.

    What it is doing is wrapping a callback interface for you.

    function getDBRecordP(id) {
      return new Promise((resolve, reject) => 
        dbLib.get(id, 
          (err, result) => err ? 
            reject(err) : 
            resolve(result)))
    }
    

    Notice in the callback example, you had to take a callback argument from the caller as a parameter, and then call that with the return value inside the callback that provides the value.

    With the Promise machinery, you do take a callback argument, but it is not provided by the caller, it is provided by the Promise that you construct. And then you return the Promise to the caller.

    The getDBRecordP function above is the essence of "how do I convert a function that takes a callback into a function that returns a Promise?".

    The caller then passes in the callback to the the Promise.then() method.

    So you have a machine that wraps the callback passing convention into a formal API that is composable, which callbacks are not.

    The other aspect of the Promise machine is the .catch() method.

    Whereas with callbacks, the convention has always been that a callback signature is (err, result), the Promise machine allows you to provide two callbacks - one for err and one for result.