31

Is it possible to chain setTimout functions to ensure they run after one another?

Penny Liu
  • 15,447
  • 5
  • 79
  • 98
xiatica
  • 1,546
  • 2
  • 20
  • 24

5 Answers5

60

Three separate approaches listed here:

  1. Manually nest setTimeout() callbacks.
  2. Use a chainable timer object.
  3. Wrap setTimeout() in a promise and chain promises.

Manually Nest setTimeout callbacks

Of course. When the first one fires, just set the next one.

setTimeout(function() {
    // do something
    setTimeout(function() {
        // do second thing
    }, 1000);
}, 1000);

Chainable Timer Object

You can also make yourself a little utility object that will let you literally chain things which would let you chain calls like this:

delay(fn1, 400).delay(fn2, 500).delay(fn3, 800);

function delay(fn, t) {
    // private instance variables
    var queue = [], self, timer;
    
    function schedule(fn, t) {
        timer = setTimeout(function() {
            timer = null;
            fn();
            if (queue.length) {
                var item = queue.shift();
                schedule(item.fn, item.t);
            }
        }, t);            
    }
    self = {
        delay: function(fn, t) {
            // if already queuing things or running a timer, 
            //   then just add to the queue
           if (queue.length || timer) {
                queue.push({fn: fn, t: t});
            } else {
                // no queue or timer yet, so schedule the timer
                schedule(fn, t);
            }
            return self;
        },
        cancel: function() {
            clearTimeout(timer);
            queue = [];
            return self;
        }
    };
    return self.delay(fn, t);
}

function log(args) {
    var str = "";
    for (var i = 0; i < arguments.length; i++) {
        if (typeof arguments[i] === "object") {
            str += JSON.stringify(arguments[i]);
        } else {
            str += arguments[i];
        }
    }
    var div = document.createElement("div");
    div.innerHTML = str;
    var target = log.id ? document.getElementById(log.id) : document.body;
    target.appendChild(div);
}


function log1() {
   log("Message 1");
}
function log2() {
   log("Message 2");
}
function log3() {
   log("Message 3");
}

var d = delay(log1, 500)
    .delay(log2, 700)
    .delay(log3, 600)

Wrap setTimeout in a Promise and Chain Promises

Or, since it's now the age of promises in ES6+, here's similar code using promises where we let the promise infrastructure do the queuing and sequencing for us. You can end up with a usage like this:

Promise.delay(fn1, 500).delay(fn2, 700).delay(fn3, 600);

Here's the code behind that:

// utility function for returning a promise that resolves after a delay
function delay(t) {
    return new Promise(function (resolve) {
        setTimeout(resolve, t);
    });
}

Promise.delay = function (fn, t) {
    // fn is an optional argument
    if (!t) {
        t = fn;
        fn = function () {};
    }
    return delay(t).then(fn);
}

Promise.prototype.delay = function (fn, t) {
    // return chained promise
    return this.then(function () {
        return Promise.delay(fn, t);
    });

}

function log(args) {
    var str = "";
    for (var i = 0; i < arguments.length; i++) {
        if (typeof arguments[i] === "object") {
            str += JSON.stringify(arguments[i]);
        } else {
            str += arguments[i];
        }
    }
    var div = document.createElement("div");
    div.innerHTML = str;
    var target = log.id ? document.getElementById(log.id) : document.body;
    target.appendChild(div);
}

function log1() {
    log("Message 1");
}

function log2() {
    log("Message 2");
}

function log3() {
    log("Message 3");
}

Promise.delay(log1, 500).delay(log2, 700).delay(log3, 600);

The functions you supply to this version can either by synchonrous or asynchronous (returning a promise).

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • I apologise for my dumb questions, but are there any internet browsers which process javascript functions concurrently, (including mobile), and if so does this ensure that they don't run concurrently? – xiatica Aug 04 '11 at 01:53
  • All browser javascript is single threaded. One thread of execution runs at a time so sequential statements are always run one after the other. Two pieces of javascript NEVER run concurrently. – jfriend00 Aug 04 '11 at 02:02
  • Added a chainable timer object with queuing and a cancel method. – jfriend00 Feb 11 '16 at 22:46
  • Can the fn argument of delay be a function that takes an argument like function logMessage(message) { log(message); }? And if so, how could I pass in the message argument? – Eugene Brown Jun 09 '16 at 06:26
  • @GeeBrownit - You can't pass `logMessage(message)` directly. You can make a wrapper function that calls `log(message)` itself. Or you can use `.bind()` to bind an argument to the callback you pass. I'd suggest you ask your own question about how to do that. – jfriend00 Jun 09 '16 at 06:50
  • 2
    Thanks, bind is sweet and works. For anyone whose interested here https://jsfiddle.net/efbbrown/ypa25gsc/1/ is how you can bind inputs to the fn argument. – Eugene Brown Jun 09 '16 at 08:30
  • Added a version using promises to handle the async sequencing for us. – jfriend00 Oct 04 '16 at 23:23
  • Why do you have `Promise.delay` and `Promise.prototype.delay`? Can you combine the 2? – guest Apr 26 '19 at 02:35
  • @guest - There are situations where you want a static function like `Promise.delay()` that returns a new promise and there are reasons why you want to insert a `.delay()` into an existing promise chain. These are separate use cases that require a slightly different wrapper around the core functionality. You can see from the code that `Promise.prototype.delay()` uses `Promise.delay()` in its implementation, but works on an existing promise chain. – jfriend00 Apr 26 '19 at 02:49
  • Added better summary of the three options listed here. – jfriend00 Nov 22 '19 at 19:42
11

Inspired by the Promise-based solution in jfriend00's answer, I demonstrated a shorter version:

Promise.resolve()
  .then(() => delay(400))
  .then(() => log1())
  .then(() => delay(500))
  .then(() => log2())
  .then(() => delay(800))
  .then(() => log3());

function delay(duration) {
  return new Promise((resolve) => {
    setTimeout(resolve, duration);
  });
}

function log1() {
  console.log("Message 1");
}

function log2() {
  console.log("Message 2");
}

function log3() {
  console.log("Message 3");
}
Syed M. Sannan
  • 1,061
  • 2
  • 9
  • 28
Penny Liu
  • 15,447
  • 5
  • 79
  • 98
6

With ES6, this is pretty simple using async/await. This is also very easy to read and a little upgrade to the promises answer.

// Expect to be async
    async function timePush(...arr){
        function delay(t){
            return new Promise((resolve,reject)=>{
                setTimeout(()=>{
                    resolve();
                },t)
            })
        }
        // for the length of this array run a delay
        // then log, you could always use a callback here
        for(let i of arr){
            //pass the items delay to delay function
            await delay(i.time);
            console.log(i.text)
        }
    }
    
    
    timePush(
        {time:1000,text:'hey'},
        {time:5000,text:'you'},
        {time:1000,text:'guys'}
    );
XMehdi01
  • 5,538
  • 2
  • 10
  • 34
1

I have encountered the same issue. My solution was to call self by setTimeout, it works.

let a = [[20,1000],[25,5000],[30,2000],[35,4000]];

function test(){
  let b = a.shift();
  console.log(b[0]);
  if(a.length == 0) return;
  setTimeout(test,b[1]);
}

the second element in array a is time to be delayed

Ankit Agarwal
  • 30,378
  • 5
  • 37
  • 62
Kenji
  • 11
  • 1
1

Using async / await with @Penny Liu example:

(async() => {
  await delay(400)
  log1()
  await delay(500)
  log2()
  await delay(800)
  log3()
})()

async function delay(duration) {
  return new Promise((resolve) => {
    setTimeout(resolve, duration);
  });
}

function log1() {
  console.log("Message 1");
}

function log2() {
  console.log("Message 2");
}

function log3() {
  console.log("Message 3");
}
Asker
  • 1,299
  • 2
  • 14
  • 31
Marco
  • 1,279
  • 1
  • 15
  • 26
  • I like this style of `delay` call, but `async` like a virus: one async, all (call stack functions) need be async – yurenchen Jun 18 '23 at 04:56