Recursive setTimeout with async/await

Viewed 2541

I'm running a setTimeout, recursively, so I only want to call the API one time per minute.

Here is my code:

;(async function ticker (minutes) {
  try {
    const res = await coingecko.global()
    const { market_cap_percentage } = res.data.data
    dominance = { btc: market_cap_percentage.btc, eth: market_cap_percentage.eth }
    console.log({ dominance })
  } catch (ex) {
    console.log(ex.message)
  } finally {
    setTimeout(ticker, minutes * 60 * 1000)
  }
})(1)

The problem is:

  • When I start my server, it calls the API immediately
  • It takes one minute to make a second call (expected behaviour)
  • After the second call, it starts calling the API sequentially, without a timeout
6 Answers

It calls it immediately because that's what your code does. It executes the ticker(1) function call immediately.

When you call ticker from the setTimeout(ticker, ...), you aren't passing the minutes parameter to that function - that's why the setTimeout() doesn't delay properly.

If you don't want it executed immediately, then get rid of the IIFE and just start it with a setTimeout(). And, then when you call ticker() from the setTimeout() callback, be sure to pass the minutes arguments to it, either by passing the third argument to setTimeout() or by making a little callback function for it.

Here's one implementation:

async function ticker(minutes) {
  try {
    const res = await coingecko.global()
    const { market_cap_percentage } = res.data.data
    dominance = { btc: market_cap_percentage.btc, eth: market_cap_percentage.eth }
    console.log({ dominance })
  } catch (ex) {
    console.log(ex.message)
  } finally {
    setTimeout(ticker, minutes * 60 * 1000, minutes);
  }
}

setTimeout(ticker, minutes * 60 * 1000, 1);

Maybe you just need to use some schedule manager, like bree?

As of your code

  1. you don't need IIFE here. Just call setTimeout(() => ticker(1), minutes * 60 * 1000)
  2. change the inner setTimeout call likewise to pass the minutes parameter, because right now you just pass undefined. That means immediately for setTimeout.

tickerexpects minutesas argument, so you have to pass the minutes when calling it inside the setTimeout. Besides that, setTimeout expects a function in the first argument, so I suggest to simply pass an arrow function which calls yours tickerfunction. Please check the following code:

;(async function ticker (minutes) {
  try {
    const res = await coingecko.global()
    const { market_cap_percentage } = res.data.data
    dominance = { btc: market_cap_percentage.btc, eth: market_cap_percentage.eth }
    console.log({ dominance })
  } catch (ex) {
    console.log(ex.message)
  } finally {
    setTimeout(() => ticker(minutes), minutes * 60 * 1000)
  }
})(1)

There are already some good explanations here, but I'm going to refactor the code a bit to take advantage of async/await without needing an external library.

First, let's make setTimeout an async function:

async function sleep(ms) {
  return new Promise(res => setTimeout(res, ms));
}

Next, let's use that instead of setTimeout:

(async function ticker (minutes) {
  do {
    await sleep(minutes * 60 * 1000);
    await loadData();
  } while (true);

  async function loadData() {
    try {
      const res = await coingecko.global()
      const { market_cap_percentage } = res.data.data
      dominance = { btc: market_cap_percentage.btc, eth: market_cap_percentage.eth }
    } catch (err) {
      console.error(ex.message);
    }
  }
})(1)

This works pretty much the same except

  1. It waits one minute once the server starts
  2. It will always wait one minute between requests

It's also a lot easier to understand and read, and since you use async/await, it sticks to a single paradigm (instead of using async/await and callbacks for the setTimeout).

I think jfriend00's answer already solved the problem. For readability sake, I advise you a less functional approach:

;(async function ticker (minutes) {
  while(true) {
    await new Promise(res => setTimeout(res, minutes * 60 * 1000));
    try {
      const res = await coingecko.global()
      const { market_cap_percentage } = res.data.data
      dominance = { btc: market_cap_percentage.btc, eth: market_cap_percentage.eth }
      console.log({ dominance })
    } catch (ex) {
      console.log(ex.message)
    }
  }
})(1)

Here is an overly complicated and probably not suited solution, setInterval. I just wanted to show how we can use setInterval to achieve the same behaviour.

const fetch = require('isomorphic-fetch')
const url = 'https://jsonplaceholder.typicode.com/posts';


const closeThis = () => {
  let ids = [];
  async function executeThisShiz(minutes) {
    ids.forEach((id) => {
      clearInterval(id);
    })
    try {
      let res = await fetch(url);
      res = await res.json();
      console.log('yay')
      return res;
    } catch(e) {
        // maybe abort the timer idk?
        console.log('error', e);
    } finally {
      id = setInterval(() => {
          executeThisShiz(minutes);
      }, minutes*60*1000);
      ids.push(id);
    }
  }
  return {
    ids,
    executeThisShiz
  }
}

const {executeThisShiz} = closeThis();
executeThisShiz(0.1);
Related