Search code examples
javascriptrecursionpromise

Waiting for all nested recursive functions to complete asynchronously


I have a recursive function which calls an asynchronous method, where the results of that method are used as arguments to call the recursive function.

I'm looking for a way to wait for all recursions to complete - but without any 'intermediate' waiting at each level. That is, recursions at all levels should run in parallel, after which a final cleanup action can occur.

I've created a minimal example which roughly matches what I'm trying to achieve.

Imagine I have a family tree API - I fetch my own record, and that returns JSON which may contain a 'parents' element. If it does, I want it to recurse and fetch all parent records, continuing up the tree.

These fetches will all fire off in parallel (with a staggered start) and take an unknown time to all return. However I want to wait until all records have been fetched before I then continue with the 'Cleanup' code.

const recurse = (url) => {
    console.log('Recurse: ', url)
    fetch(url).then((res) => {
      db.write(res.json)
      if ('parents' in res.json) {
        for (let parent of Object.values(res.json.parents)) {
          recurse(parent.URL)
        }
      }
    })
  }
}


const main = () => {
  let data = 'http://api.familytree.example.com/me'
  console.log('Start')
  recurse(data)
  console.log('Cleanup')
}

main()


Solution

  • Recursion with promises can be tricky, but async/await will make it much easier. Here is an example of using recursion with async functions:

    const recurse = async (url) => {
      console.log('Recurse: ', url)
      const personData = await fetch(url).then(res => res.json());
      const results = [personData];
      if ('parents' in personData) {
        const parentCalls = [];
        for (let parent of personData.parents) {
          parentCalls.push(recurse(parent.URL).then(ancestors => results.push(...ancestors)));
        }
        await Promise.all(parentCalls);
      }
      
      return results;
    };
    
    
    const main = async () => {
      console.log('Start')
      const data = await recurse('http://api.familytree.example.com/me');
      db.write(data);
      console.log('Cleanup')
    }
    
    main()
    

    The fundamental approach is to write a function (recurse) that gets the current data and return a list of results. Once you have that then you can modified it to check for any parent data. If there is parent data then call recurse for each of the parents and add the resulting list to the current list of results. The root call will return an array with every item in there.

    Some other things to think about:

    • Handling duplicates (in this case parents should not duplicate).
    • Recursion limits (not required but can be good practice).
    • Including relationship data in results (might already be included in response data).