The importance of returning within a function when promises are involved

Intro

Promises are a relatively new and incredibly important part of Javascript and Typescript. Since they are widely used, no need to explain what they do and how to use them, I’ll just highlight how to avoid what might cause bugs in the future.

The problem

Whenever you use an async function javascript always returns a Promise. Even if the function is empty, the signature of this function will always be a Promise. This does not mean, though, that any async job inside the function will be resolved before this promise is resolved!

It’s important to check that (if needed) every promise inside the function is awaited or at least returned in order to be safe. With some code it will be much clearer.

The code

This is a simple example:

function wait(ms: number): Promise<void> {
  return new Promise<void>(resolve => setTimeout(resolve, ms))
}

async function delayedLog(message: string): Promise<void> {
  await wait(1000)
  console.log(message)
}

async function sendMessage(message: string, ms: number): Promise<void> {
  await wait(ms)
  delayedLog(message, ms)
}

async function main(): Promise<void> {
  await sendMessage('first', 1000)
  await sendMessage('second', 100)
}

main()

Looking at this code one might expect that this program will log first after (1000ms + 1000ms) = 2s and only then it will log second after another (1000ms + 100ms) = 1.1s.

Actually, the code will log second and then first. This might seem strange (or not, I think it does) since we’re awaiting for the first promise to resolve before starting the second. And if you check the signature of sendMessage, you get Promise<void>, and that’s correct!

What sendMessage is actually missing is the return: while it returns a Promise, that Promise is resolved right after delayedLog is called, without waiting for it to resolve!

The solution

The solution is really simple:

function wait(ms: number): Promise<void> {
  return new Promise<void>(resolve => setTimeout(resolve, ms))
}

async function delayedLog(message: string): Promise<void> {
  await wait(1000)
  console.log(message)
}

async function sendMessage(message: string, ms: number): Promise<void> {
  await wait(ms)
  return delayedLog(message, ms)
}

async function main(): Promise<void> {
  await sendMessage('first', 1000)
  await sendMessage('second', 100)
}

main()

The code seems exactly the same, but this time sendMessage returns the Promise generated by delayedLog. This means that the Promise returned by sendMessage is resolved only when the promise returned by delayedLog is resolved too, and this way, the order of the logs is the expected one.

The cause

While I cannot say for certain what the cause of something like this might be, I guess that the only way to avoid something like this to happen would be to decorate Promises with some information on what instantiated them, since the signature is not enough to recognize if a Promise is the correct one.