Send multiple REST requests at the same time. (node) Below is a (very) simplified version of a function I made that creates multiple REST requests to gather needed information.
Things to note:

One of the REST calls is dependent upon the output of another.
I want to send off multiple requests at the same time when possible
If any rest responses inform us that the user is logged out, null should be returned.

async function gatherUserInfo(userId) {
  try {
    // Don't await yet, so this request can be sent at the same time as future requests.
    const groupsRequest = getGroups(userId)

    const {username, name} = await getProfile(userId)
    return {
      username,
      name,
      statusText: await getStatusText(username),
      groups: await groupsRequest,
    }
  } catch (err) {
    if (err instanceof NotLoggedInError) {
      return null
    }
    throw err
  }
}

I found the above code to be a very elegant solution to the problem. However, I recently realized that it won't actually work in node, because node will give warning or stop execution (depending on version) if an uncaught error happens in a promise. In the example above, the getGroups() call would send off a request (but not await it). Then, this function execution will pause on getProfile(). In the meantime, getGroups() will come back with the NotLoggedInError, which will result in the process being terminated, because that promise has not been awaited yet, (the try-catch can't catch the error until the await happens, which does happen later in the function execution).
So, I reluctantly trashed my old design and coded this up instead:
async function gatherUserInfo(userId) {
  const requests = [];

  requests.push(
    getGroups(userId)
    .then(groups =&gt; ({groups}))
  )

  requests.push(
    getProfile(userId)
    .then(async ({username, name}) =&gt; {
      const statusText = await getStatusText(username)
      return {username, name, statusText}
    })
  )

  let responses
  try {
    responses = await Promise.all(requests)
  } catch (err) {
    if (err instanceof NotLoggedInError) {
      return null
    }
    throw err
  }

  return Object.assign({}, ...responses)
}

This is much harder to follow or understand, but it's the best I could come up with. How can this code be improved?
Here's some stub methods and test cases that can be paired with the examples above to execute them:
// Mocks //

async function getGroups(userid) {
  await wait(50)
  if (userid !== 12) throw new NotLoggedInError('User not logged in')
  return ['group A', 'group B'];
}

async function getProfile(userid) {
  await wait(30)
  if (userid !== 12) throw new NotLoggedInError('User not logged in')
  return { username: 'grinchMaster135', name: 'The Grinch' };
}

async function getStatusText(username) {
  await wait(30)
  return 'away'
}

class NotLoggedInError extends Error {}

const wait = ms =&gt; new Promise(resolve =&gt; setTimeout(resolve, ms))


// TEST //

;(async function() {
  console.log('Grinch info:', await gatherUserInfo(12)) // Grinch info: { username: 'grinchMaster135', name: 'The Grinch', groups: [ 'group A', 'group B' ], statusText: 'away' }
  console.log('Non-logged in user info:', await gatherUserInfo(13)) // Non-logged in user info: null
})()
```
When you have multiple asynchronous requests to make, and you want them to operate in parallel, usually the appropriate tool to use is Promise.all. You can use it here to wait for the groups and the profile data at once, despite the fact that the two requests are pretty distinct from each other:
async function gatherUserInfo(userId) {
  try {
    const [groups, {username, name}] = await Promise.all([
      getGroups(userId),
      getProfile(userId),
    ])
    return {
      username,
      name,
      groups,
      statusText: await getStatusText(username),
    }
  } catch (err) {
    if (err instanceof NotLoggedInError) {
      return null
    }
    throw err
  }
}

This way, if any one of the Promises rejects, it will be being awaited at that time, so it'll be caught properly.


async function gatherUserInfo(userId) {
  try {
    const [groups, {username, name}] = await Promise.all([
      getGroups(userId),
      getProfile(userId),
    ])
    return {
      username,
      name,
      groups,
      statusText: await getStatusText(username),
    }
  } catch (err) {
    if (err instanceof NotLoggedInError) {
      return null
    }
    throw err
  }
}



async function getGroups(userid) {
  await wait(50)
  if (userid !== 12) throw new NotLoggedInError('User not logged in')
  return ['group A', 'group B'];
}

async function getProfile(userid) {
  await wait(30)
  if (userid !== 12) throw new NotLoggedInError('User not logged in')
  return { username: 'grinchMaster135', name: 'The Grinch' };
}

async function getStatusText(username) {
  await wait(30)
  return 'away'
}

class NotLoggedInError extends Error {}

const wait = ms =&gt; new Promise(resolve =&gt; setTimeout(resolve, ms))


// TEST //

;(async function() {
  console.log('Grinch info:', await gatherUserInfo(12)) // Grinch info: { username: 'grinchMaster135', name: 'The Grinch', groups: [ 'group A', 'group B' ], statusText: 'away' }
  console.log('Non-logged in user info:', await gatherUserInfo(13)) // Non-logged in user info: null
})()



You could also have fixed the unhandled rejection by adding a no-op .catch handler to the Promise that was created before awaiting the other Promise:
groupsRequest.catch(() =&gt; void 0);

But that makes the code less elegant - I think the Promise.all version is better.


async function gatherUserInfo(userId) {
  try {
    // Don't await yet, so this request can be sent at the same time as future requests.
    const groupsRequest = getGroups(userId)
    groupsRequest.catch(() =&gt; void 0);

    const {username, name} = await getProfile(userId)
    return {
      username,
      name,
      groups: await groupsRequest,
      statusText: await getStatusText(username),
    }
  } catch (err) {
    if (err instanceof NotLoggedInError) {
      return null
    }
    throw err
  }
}



async function getGroups(userid) {
  await wait(50)
  if (userid !== 12) throw new NotLoggedInError('User not logged in')
  return ['group A', 'group B'];
}

async function getProfile(userid) {
  await wait(30)
  if (userid !== 12) throw new NotLoggedInError('User not logged in')
  return { username: 'grinchMaster135', name: 'The Grinch' };
}

async function getStatusText(username) {
  await wait(30)
  return 'away'
}

class NotLoggedInError extends Error {}

const wait = ms =&gt; new Promise(resolve =&gt; setTimeout(resolve, ms))


// TEST //

;(async function() {
  console.log('Grinch info:', await gatherUserInfo(12)) // Grinch info: { username: 'grinchMaster135', name: 'The Grinch', groups: [ 'group A', 'group B' ], statusText: 'away' }
  console.log('Non-logged in user info:', await gatherUserInfo(13)) // Non-logged in user info: null
})()