Search code examples
javascripttypescriptasync-awaitpromise

Async await - nested then depending on result of previous await


Consider the following non-negotiables:

There are 3 async functions that I have to work with and they're below, I cannot change these.

    async asyncCall1({ caseId }: { caseId: string }) {
        const response = await this.apolloClient.query({...})
        return {
          ...response,
          data: caseResponse,
        }; 
    }


    async asyncCall2({ caseIdFromCall1 }: { caseIdFromCall1: string }) {
        const response = await this.apolloClient.query({...})
        return {
          ...response,
          data: caseResponse,
        }; 
    }

    async asyncCall3({ caseIdFromCall3 }: { caseIdFromCall3: string }) {
        const response = await this.apolloClient.query({...})
        return {
          ...response,
          data: caseResponse,
        }; 
    }

What I need happen is:

  1. Need one field from response of asyncCall1 to feed into asyncCall2, I then need one field from asyncCall2 to feed as input to asyncCall 3. Essentially the calls have to happen sequentially.

  2. I need to do add to a list from asyncCall1 and asyncCall2 and asyncCall3 and return a boolean based on a condition while processing asyncCall3.

  3. A way to handle exceptions in each? - Perhaps return a certain boolean value in each case we run into an error in any of the calls.

  4. I'm happy if a solution gives me a way to move away from nested hell

Here is my try

    const checkAuthorization = (input: string) => {

        let coalate = []

        dependency1.asyncCall1({ caseId: input })
            .then((call1RawResult) => {
              console.log("Inside caseResult: ", call1Result);
              return { data: call1Result.data.caseProperties.caseId1 }; //This is the response from call 1 that I want to pass to call 2
            })
            .then((call1SpecificField) => dependency2.asyncCall2({caseId: call1SpecificField.data})) //This is where I attempt to use value from asyncCall1
            .then((call2RawResult) => {
              const caseId2 = call2RawResult.data.caseProperties.caseId2; 
              coalate.push(caseId2); //Also push to list
              return { data: caseId2 } //Do some operation and pass this on for use in the next call
            })
            .then((call2SpecificField) => dependency3.asyncCall3({caseId: call2SpecificField.data}))
            .then((call3RawResult) => {
              const caseId3 = call3RawResult.data.caseProperties.caseId2; 
              coalate.push(caseId3); //Also push to list
              
              //Finally return true or false if input is found in consolidated coalate list
              return coalate.includes(input);
            }).catch((error) => { //If any issue in any of the above dependency calls - return false.
              console.log(error)
              return false;
            })

     }
  1. My issue is that I seem to be hitting the line Inside caseResult:, but never beyond that.
  2. When calling checkAuthorization(), I need the computed boolean value from inside the catch block atleast, but it just comes out as undefined.

Solution

    • It's 2023 - using await instead of .then() callback-hell results in code that's much easier to read, write, and understand, in my opinion.
      • BTW, using await does not mean you have to await each Promise when they enter the scope: it's perfectly valid to store a returned/received Promise in a local and then await that local later-on - this is how you can implement certain kinds of nontrivial Promise logic.

    I'll tackle each of your goals individually and incrementally:

    • Need one field from response of asyncCall1 to feed into asyncCall2.
    • I then need one field from asyncCall2 to feed as input to asyncCall3.
    • Essentially the calls have to happen sequentially.

    That's straightforward:

    (btw, svc: MyService is the object with those asyncCallN methods).

    async function run( svc: MyService, input: string ) {
    
        const result1 = await svc.asyncCall1( { caseId: input } );
        console.log( "result1: %o", result1 );
        const caseId1 = result1.data.caseProperties.caseId1;
    
        const result2 = await svc.asyncCall2( { caseId: caseId1 } );
        console.log( "result2: %o", result2 );
        const caseId2 = result2.data.caseProperties.caseId2;
    
        const result3 = await svc.asyncCall3( { caseId: caseId2 } );
        console.log( "result3: %o", result3 );
        const caseId3 = result3.data.caseProperties.caseId2; // <-- Not `caseId3` ?
    }
    

    I need to do add to a list from asyncCall1 and asyncCall2 and asyncCall3 and return a boolean based on a condition while processing asyncCall3.

    Again, straightforward:

    (though I feel a Set<string> should be used instead of an Array<string> / String[] because your code will be performing set-membership checks, which are O(1) (good) for Set<T> but O(n) (bad) for Array<T>).

    async function run( svc: MyService, input: string ): Promise<boolean> {
        const caseIdsSet = new Set<string>();
    
        const result1 = await svc.asyncCall1( { caseId: input } );
        console.log( "result1: %o", result1 );
        const caseId1 = result1.data.caseProperties.caseId1;
        caseIdsSet.add( caseId1 );
    
        const result2 = await svc.asyncCall2( { caseId: caseId1 } );
        console.log( "result2: %o", result2 );
        const caseId2 = result2.data.caseProperties.caseId2;
        caseIdsSet.add( caseId2 );
    
        const result3 = await svc.asyncCall3( { caseId: caseId2 } );
        console.log( "result3: %o", result3 );
        const caseId3 = result3.data.caseProperties.caseId2; // <-- Not `caseId3` ?
        caseIdsSet.add( caseId3 );
    
        return caseIdsSet.has( input );
    }
    

    A way to handle exceptions in each? - Perhaps return a certain boolean value in each case we run into an error in any of the calls.

    Only catch exceptions if you actually need to: Unless you expect any of those asyncCallN calls to fail during normal program operation and your program needs to do something during an error condition (such as rolling-back any persisted changes so-far) then you don't need to litter your program code with low-value catch statements.

    Even if you want to just want to log any errors (e.g. catch( err ) { console.error(err); }) you still shouldn't normally need any catch blocks: most JS environments will/can still log exceptions at the point at which they are thrown by a throw statement. If this is a JS that runs in a web-browser then you should also be listening to window's 'error' event for unhandled JS exceptions.


    Your asyncCallN functions seem to be using Apollo - if so, then you should (largely) ignore the vague, general advice that I wrote above and instead follow Apollo's guide to error-handling with their library.


    I'm happy if a solution gives me a way to move away from nested hell

    This answer.