JavaScript: setTimeout() and setInterval()

Scheduling a call

2 methods:
  • setTimeout(): Run a function once after the time interval
  • setInterval(): Run a function repeatedly. Starting after the time interval, and repeating with that time interval.

Asynchronous

Asynchronous function
Both are asynchronous functions. Meaning: the timer function will not pause execution of other functions in the function stack. It doesn't delay the start of the next timeout/interval.
setInterval(() => {console.log("Tic")}, 1000)
setTimeout(() => {console.log("Message 1")}, 2000);
setTimeout(() => {console.log("Message 2")}, 4000);
setTimeout(() => {console.log("Message 3")}, 6000);
The message 1, 2, and 3 will appear at second 2, 4, and 6. Instead of 2, 6, and 12.
Results:
Second 1: Tic
Second 2: Tic, Message 1
Second 3: Tic
Second 4: Tic, Message 2
Second 5: Tic
Second 6: Tic, Message 2
Second 7: Tic
Second 8: Tic (until cleared)

setTimeout()

setTimeout() syntax
setTimeout(function, delayMs, ...)
This method calls a function after a number of milliseconds.
Arguments:
  • Function to execute (required). It can be a quoted code instead.
    • Remember: Don't write the function with brackets funcName(), because it will run the code right away.
    • For function with parameters, how to include the parameters then if bracket not the way to go? List them as a comma-separated list after the time delay. See "other parameters" below.
  • Delay in milliseconds (optional)
    • Default value is 0. To be executed immediately (the next event cycle).
    • Exact delay is not guaranteed
      • CPU overloaded
      • Browser tab is in background mode
      • laptop is on battery
      • etc
    • Minimum delay time if 4 ms is enforced for 5th and above nested timeout and interval.
    • If the delay is not in number, type coercion will be done to convert it.
      • "1000" is coerced into 1000
    • Don't rely on type coercion though.
      • "1 second" is coerced into 0
    • Just use number.
  • Maximum delay value
    • Browsers store delay as 32 bit signed integer internally. Interger above 2,147,483,647 ms (about 24.8 days) will cause integer overflow and as result, timeout will be executed immediately.
  • Other parameters need to be passed to the function (optional). For example:
    • function greetName(phrase, name) {...}
    • setTimeout(greetName, 5000, "The phrase", "The name")
  • Return value:
    A number, which is the id of the timer. Id is for use in clearTimeout(id) to cancel the timer.
    Extended delays
    • Nested timeout/setinterval: minimum 4ms for 5th call and above
    • Timeout in inactive tabs
    • Throttling of tracking scripts
    • Late timeout
    • Deferral of timeouts during pageload
    Read more: MDN: Timeout in inactive tabs and more
    Maximum delay value
    Browsers store delay as 32 bit signed integer internally. Interger above 2,147,483,647 ms (about 24.8 days) will cause integer overflow and as result, timeout will be executed immediately.
    let counter = 0;
    let tic = setInterval(() => {
    counter ++;
    console.log(counter);
    }, 1000);
    let underLimit = setTimeout(() => {
    console.log("Under limit");
    }, 2147483647)
    let overLimit = setTimeout(() => {
    console.log("Over limit");
    }, 2147483648);
    console.log(counter); 
    
    Result:
    0
    Over limit
    1
    2
    3
    ... // the underlimit will get executed in 24-25 days if doesn't get truncated.
    
    Simplest example:
    function greetJello() {
        console.log("Hello, Jello!");
    } 
    
    setTimeout(greetJello, 5000);
    // After 5 seconds: "Hello Jello!"
    
    Example inside function:
    function greetLater() {
        setTimeout(greetNow, 5000);
    } 
    
    function greetNow() {
        alert("Greetings!");
    } 
    
    greetNow();
    // Right away: "Greetings!"
    
    greetLater();
    // After 5 seconds: "Greetings!"
    
    Example with extra parameters:
    function greetName(phrase, name) {
        console.log(phrase + ", " + name + "!");
    } 
    
    setTimeout(greetName, 5000, "Hi", "Kitty");
    // After 5 seconds: "Hi, Kitty!"
    
    Don't use brackets: setTimeout(myFunction(), 5000)
    It's the same with calling a function. Function will be executed immediately instead of delayed.
    function greetJello() {
        console.log("Hello, Jello!");
    } 
    
    setTimeout(greetJello, 5000);
    // After 5 seconds: "Hello Jello!"
    
    setTimeout(greetJello(), 5000);
    // Right away: "Hello Jello!"
    

    setTimeout with quoted code (not recommended)

    setTimeout can be written with a quoted code directly
    Syntax
    setTimeout("code", delayMS, ...)
    
    Code must be quoted
    • Why? setTimeout needs a reference to the code, not the code directly.
    • Without quotation, it's a code call. Will be executed right away.
    • Similarly, when function is use, we can't write the function with the brackets, because it will be a function call.
    Compare the wrong way
    No delay. Code/function is called and executed right away.
    • setTimeout(alert("Hi"), 5000)
    • setTimeout(myFunction(), 5000)
    Compare the right way
    Code/function is referred.
    • setTimeout("alert('Hi')", 5000)
    • setTimeout(myFunction, 5000)
    Example
    setTimeout("alert('Hi!')", 5000);
    // After 5 seconds: "Hi!"
    
    Quoted code not recommended: Use function
    As an anonymous arrow function
    setTimeout(() => alert('Hi!'), 5000);
    // After 5 seconds: "Hi!"
    

    setInterval

    setInterval()
    Executing a function repeatedly with a given interval.
    Syntax
    Very similar to setTimeout()
    setInterval(function, intervalMs, ...)
    
    Check out setTimeout() for further arguments and return value.
    First call
    The first function call happened after the first time delay instead of before. This is like doing setTimeout but again and again.
    Time goes on
    Time goes on when running setInterval. The delay time marking the starting point of function call. In a run where user respond is required like alert, confirm, and prompt, it didn't wait for the user response to click to start delay count. The delay is to the start of the function call, not to the end of the function run.
    Example: counter
    let counter = 0;
    function counterFn() {
        counter++;
        console.log(counter);
    }
    
    const myCounter = setInterval(counterFn, 1000);
    
    // 1 
    // 2
    // 3
    // 4
    // 5 (run continuously until cleared
    
    clearInterval(myCounter);   // Execution cleared
    

    clearTimeout and clearInterval

    clearTimeout() and clearInterval()
    These commands stops a running timeout or interval.
    In the case of timeout, if it's run before the time delay is reached, it's essentially cancelling the timeoutFn from running.
    clearTimeout() syntax
    const timeoutId = setTimeout(timeoutFn, ms)
    // return an id number, for example 1
    
    clearTimeout(timeoutId);
    clearTimeout(1);
    
    clearInterval() syntax
    const intervalId = setInterval(intervalFn, ms)
    // return an id number, for example 2
    
    clearInterval(intervalId);
    clearInterval(2);
    
    ID number
    When a setTimeout() or setInterval() is run, it returns an id.
    In most cases, this id is a consecutive integer start from 1, give according to the order of the timeout/interval call.
    This id number is what's used to clear the timeout/interval
    Same ID pooling for both setTimeout() and setInterval()
    Example: when a webpage is opened:
    • First a setTimeout() is run. It gets id 1.
    • Then a setInterval() is run. it gets id 2 eventhough it's the first interval to be run, because of the same numbering pooling with timeout counterpart.
    Assigned a constant for each id
    Problem in using the id number directly:
    • Some browser gives different numbering system.
      • Some starts from 2 (Firefox, Opera)
      • Some gives random 6-digit number (IE)
      • Some starts over when a new tab is opened (Opera), and some have a global count acrross the tabs opened (Safari).
    • It's hard to know which one running first. Also when we tweak the code, we have to adjust all numbering accordingly? That's a very dangerous practise.
    • But even when we know exactly order of running. Or even more sure, we only have exactly 1 timeout/interval running. We still can't use it directly.
    What to do, then?
    • Assign a constant to the timeout and interval when we run it. As simple as:
      • const timeoutId = setTimeout(fn, ms)
      • const intervalId = setInterval(fn, ms)
    • This way, those numbers, 1, 2, or even whatever random 6 digit number can be addressed as the constant name safely. We don't care the exact number. We refer it using the constant name.
    clearTimeout() and clearInterval() are interchangable
    Both can be used to clear both timeout and interval. However don't do this. It's a bad practise, it makes the code less readable.
    Example
    Use clearTimeout() to cancel a timeout function from running.
    const timeoutId = setTimeout(() => console.log("Hello!"), 5000);
    
    function stopGreeting() {
        clearTimeout(timeoutId);
    }
      
    stopGreeting();
    // After 5 seconds: nothing
    

    Nested timeout

    Set interval with nested setTimeout()
    Timeout function is called from within the function itself.
    Advantage over setInterval
    • Time delay can be manipulated
    • Delay is more realistic. It starts after the execution is finished instead of just working periodically. For a function that takes a long time to run, or waiting for user feedback for completion, this will be prominent.
    setInterval
    let idName = setInterval(() => alert("Next alert in 2 seconds. Agree?"), 2000);
    
    Nesting setTimeout()
    let idName = setTimeout(function fnName() {
        alert("Next alert in 2 seconds. Agree?");    
        idName = setTimeout(fnName, 2000);
    }, 2000);
    
    Compare the two above
    Try to wait for 10 seconds to click the OK.
    • setInterval: jammed. Alert messages have been waiting.
    • Nested setTimeout: Nicely distanced 2 seconds apart from clicking.
    User can even decide the delay in nesting setTimeout()
    let idName = setTimeout(function fnName() {
        let delay = prompt(`Next alert in how many seconds?
    Max = 5s
    Default = 1s`);
        if (delay === null) {
            return;
        } else if (!(delay >= 0 && delay <= 5)) {
            delay = 1;
        }
        idName = setTimeout(fnName, delay*1000);
    }, 1000);
    
    Example with time delay manipulation
    Fire request every 1 second. But if request is rejected due to server overload, double the time delay from the last. Give up if delay is larger than 2 minutes.
    let delay = 0;
    let reject = true;
    let maxDelay = 15000;
    
    let requestId = setTimeout(function request() {
        if (reject) {
            console.log(`Request rejected. Last waiting time: ${delay/1000} s.`)
            if (delay < maxDelay) {
                delay = delay + 5000;
                console.log(`Resend request in ${delay/1000} s.`);
                let counter = 0;
                let counterId = setInterval(() => {
                    counter ++;
                    console.log(counter);
                }, 1000)
                let stopCount = setTimeout(() => {
                    clearInterval(counterId);
                }, delay)
                requestId = setTimeout(request, delay);
            } else {
                console.log(`Max waiting time ${maxDelay/1000} s. Request cancelled. `);
                clearTimeout(requestId);
            } 
            
        } else {
            console.log("Request fulfilled. Request function is stopped.");
            clearTimeout(requestId);
        }
    }, delay);
    
    For fun, interject with:
    • reject = false;
    • maxDelay = 10000;
    Nested timeout get closed properly? No
    Uncaught RangeError: Maximum call stack size exceeded
    Let's do simple "Hi" with zero delay to be extreme
    let nestLimit = setTimeout(function hi() {
        console.log(`Hi`);
        nestLimit = setTimeout(hi())
    })
    
    let intLimit = setInterval(function hi() {
        console.log(`Hi`);
    })
    
    Results
    Nested setTimeout
    // (10296) Hi
    // Uncaught RangeError: Maximum call stack size exceeded
    
    Regular setInterval
    // (100K) Hi 
    // ... and still going
    
    Same result with delay 1ms, 2ms, 5ms, 10ms. Didn't try longer delay, do not want to wait longer.

    Example: Timer with counter and cancel button

    About
    Timer with counter and cancel button
    Methods featured
    setTimeout, setInterval, clearTimeout, clearInterval
    Codes and result
    HTML codes
    <div>
        <label for="timeInput2">Enter time (0-60 seconds): </label>
        <input type="text" id="timeInput2" value="3" size="2">
        <input type="submit" id="timerButton2" value="Start timer">
    </div>  
    <p id="timerPar" style="display: none;"></p>
    <p id="counterPar" style="display: none;"></p>
    <p id="ringerPar" style="display: none;">Ring Ring Ring!</p>
    
    <div id="cancelBox" style="display: none">
        <input type="submit" id="cancelButton" value="Cancel timer">
    </div>
    <p id="cancelComment" style="display: none;">Timer cancelled.</p>
    <div id="refreshBox" style="display: none">
        <input type="submit" id="refreshButton" value="Refresh timer">
    </div>
    
    JS Codes
            
    let myTimer;
    let myInterval;
    
    function timerFunction2() {
        refreshTimer();                                    
        const timeLimit2 = document.getElementById("timeInput2").value;                                    
    
        if (timeLimit2 >= 0 && timeLimit2 <= 60) {
            let counterText = "";
            let counter = 0;
            document.getElementById("timerButton2").disabled = true;
            document.getElementById("timerPar").textContent = `Timer is set for ${timeLimit2} seconds.`;
            document.getElementById("timerPar").style.display = "block";
            document.getElementById("counterPar").textContent = counterText;
            document.getElementById("counterPar").style.display = "block";
            document.getElementById("cancelBox").style.display = "block";
    
            myInterval = setInterval(() => {
                counter ++;
                counterText += `${counter} `;
                document.getElementById("counterPar").textContent = counterText;
            }, 1000)
    
            myTimer = setTimeout(() => {
                clearInterval(myInterval);
                document.getElementById("ringerPar").style.display = "block";
                document.getElementById("cancelBox").style.display = "none";
                document.getElementById("refreshBox").style.display = "block";
                document.getElementById("timerButton2").disabled = false;
            }, timeLimit2*1000);
        } 
    }
    
    function cancelTimer() {
        clearTimeout(myTimer);
        clearInterval(myInterval);
        document.getElementById("cancelBox").style.display = "none";
        document.getElementById("cancelComment").style.display = "block";
        document.getElementById("refreshBox").style.display = "block";
        document.getElementById("timerButton2").disabled = false;
    }
    
    function refreshTimer() {
        document.getElementById("counterPar").textContent = "";
        document.getElementById("timerPar").textContent = "";
        document.getElementById("cancelBox").style.display = "none";
        document.getElementById("refreshBox").style.display = "none";
        document.getElementById("timerPar").style.display = "none";
        document.getElementById("counterPar").style.display = "none";
        document.getElementById("cancelComment").style.display = "none";
        document.getElementById("ringerPar").style.display = "none";
        document.getElementById("timerButton2").disabled = false;
    }
    
    document.getElementById("timerButton2").addEventListener("click", timerFunction2);
    document.getElementById("cancelButton").addEventListener("click", cancelTimer);
    document.getElementById("refreshButton").addEventListener("click", refreshTimer);
    

    Delay 0

    Delay 0 (default)
    It's possible to have delay = 0 ms. In fact, that's the default when delay parameter is not given. In this case, it will be running immediately without delay.
    setTimeout(fn)
    setTimeout(() => console.log("Timeout with 0 delay"));
    
    setInterval(fn)
    Get ready for interval running very quicly and forever. Terminate with clearInterval(id)
    let intZero = setInterval(() => {
        console.log("Interval with 0 delay. Terminate: clearInterval(intZero)");
    });
    
    setInterval(fn) with auto-termination: internal counter
    let counter = 0;
    let intZeroCount = setInterval(() => {
        counter ++;
        console.log("Interval with 0 delay. Auto-termination at 1000th run.");
        if (counter === 1000) {
            clearInterval(intZeroCount);
        }
    });
    
    setInterval(fn) with auto-termination: external setTimeout
    let intZeroTimed = setInterval(() => {
        console.log("Interval with 0 delay. Auto-termination at 2s with external setTimeout");
    });
    
    setTimeout(() => {
        clearInterval(intZeroTimed);
    }, 2000)
    
    4 milliseconds in browser
    Timers can be nested; five and above such nested timers, however, the interval is forced to be at least four milliseconds.
    Same true for setInterval
    Why? Limitation happens on earlier time, and many scripts rely on it. So it is kept this way.
    Such limitation doesn't exist in server-side Javascript. For doing proper immediate asynchronous job though, use other method, that's unfortunately rather browser-specific. Like setImmediate for Node.js
    let start = Date.now();
    let counter = 0;
    let timeoutArray = [];
    setTimeout(function check() {
        timeoutArray.push(Date.now() - start);
        start = Date.now();
        counter ++;
        if (counter < 20) {setTimeout(check)}
    });
    
    timeoutArray;
    // (20) [2, 1, 1, 1, 4, 4, 5, 4, 4, 4, 5, 4, 4, 4, 5, 4, 4, 4, 5, 4]
    
    Interval counterpart: similar
    let start = Date.now();
    let counter = 0;
    let intervalArray = [];
    let intCheck = setInterval(function check() {
        intervalArray.push(Date.now() - start);
        counter ++;
        if (counter === 20) {clearInterval(intCheck)}
    });
    
    intervalArray;
    // (20) [2, 0, 1, 2, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4]
    

    Invocation time

    Invoked after current script execution is finished.
    Whether the delay is zero or not, setTimeout and setInterval invocations are after the current script execution is finished.
    Consequence:
    Even when setTimeout and setInterval are written first, it will have to wait the codes after to run.
    Simple example
    setTimeout(() => alert("Timeout"));
    alert("Current script");
    
    Fun example
    See how setTimeout and setInterval are written first but run last.
    // Code 1: setTimeout
    setTimeout(() => console.log("Code 1: Timeout set at 5000."),5000);
    
    // Code 2: setInterval
    counter = 0;
    let intervalId = setInterval(() => {
        counter++;
        console.log("Code 2: Interval " + counter);
        if (counter === 10) {
            clearInterval(intervalId);
        }
    }, 1000)
    
    // Code 3: plain console log
    console.log("Code 3: Plain console log run.");
    
    // Code 4: for-loop and result
    let i = 1;
    for(i; i <= 1000; i++) {
        i *= i
    };
    console.log("Code 4: For-loop multiplication = " + i);
    
    //Code 5: function
    function multiply(a, b) {
        return a*b;
    }
    console.log("Code 5: function multiply = " + multiply(100, 100))
    
    Order of result in console:
    Code 3: Plain console log run.
    Code 4: For-loop multiplication = 458330
    Code 5: function multiply = 10000
    Code 2: Interval 1
    Code 2: Interval 2
    Code 2: Interval 3
    Code 2: Interval 4
    Code 1: Timeout set at 5000.
    Code 2: Interval 5
    Code 2: Interval 6
    Code 2: Interval 7
    Code 2: Interval 8
    Code 2: Interval 9
    Code 2: Interval 10
    
    If we switch code 1 and 2, into interval first, then in the result, timeout will appear after interval 5 as expected.
    Compare: setTimeout outside function, inside function, and function without timeout
    setTimeout("console.log('Timeout outside function')", 5000);
    function funcTimey() {
        setTimeout("console.log('Function with timeout inside')", 5000)
    };
    funcTimey();
    function funcLoggy() {
        console.log('Function without timeout')
    };
    funcLoggy();
    
    Order of result in console:
    // Immediately
    Function without timeout  
    
    // The same time, after 5 seconds:
    Timeout outside function
    Function with timeout inside
    

    Example: Counter

    About
    Counter with start and stop button
    Codes and result
    0
    HTML Codes
    <div>
        <input type="button" id="startCounter" value="Start counting">
        <input type="button" id="stopCounter" value="Stop counting">
        <input type="button" id="backToZero" value="Back to Zero">
    </div>                            
    <div id="counterBox" size="5" style="font-size:2rem; color: pink;">0</div>
    
    JS Codes
    let coolCounter;
    let number = 0;
    let counterOn = false;
    
    function startCount() {
        if (!counterOn) {
            coolCounter = setInterval(() => {
            number ++;
            document.getElementById("counterBox").textContent = number;
        }, 1000)
        }                                    
        counterOn = true;
    }
    
    function stopCount() {
        clearInterval(coolCounter);
        counterOn = false;
    }
    
    function zeroNumber() {
        clearInterval(coolCounter);
        counterOn = false;
        number = 0;
        document.getElementById("counterBox").textContent = number;
    }
    
    document.getElementById("startCounter").addEventListener("click", startCount);
    document.getElementById("stopCounter").addEventListener("click", stopCount);
    document.getElementById("backToZero").addEventListener("click", zeroNumber);
    

    Calendar date and time

    Calender updated each second
    : :
    HTML Codes
    <div class="calendar">
        <span id="currentDay" style="margin: auto;"></span>
    </div>
    <div class="calendar">
        <span id="currentDate"></span> <span id="currentMonth"></span> <span id="currentYear"></span>
    </div>
    <div class="calendar">
        <span id="currentHour"></span> : <span id="currentMinute"></span> : <span id="currentSecond"></span>
    </div>  
    
    JS Codes
    const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
    const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
    
    function getName (array, index) {
        return array[index];
    }
    
    Alternative 1: Direct HTML element assignments
    function updateDisplay1() {
        const timeStamp = new Date();
        document.getElementById("currentSecond").textContent = timeStamp.getSeconds();
        document.getElementById("currentMinute").textContent = timeStamp.getMinutes();
        document.getElementById("currentHour").textContent = timeStamp.getHours();
        document.getElementById("currentDate").textContent = timeStamp.getDate();
        document.getElementById("currentMonth").textContent = getName(months, timeStamp.getMonth());
        document.getElementById("currentYear").textContent = timeStamp.getFullYear();
        document.getElementById("currentDay").textContent = getName(days, timeStamp.getDay());
    }
    
    updateDisplay1();
    setInterval(updateDisplay1, 1000);
    
    Alternative 2: HTML element assignments through array
    const calendarDisplay = [
        { id: "currentSecond", content: (timeStamp) => timeStamp.getSeconds()},
        { id: "currentMinute", content: (timeStamp) => timeStamp.getMinutes()},
        { id: "currentHour", content: (timeStamp) => timeStamp.getHours()},
        { id: "currentDate", content: (timeStamp) => timeStamp.getDate()},
        { id: "currentMonth", content: (timeStamp) => getName(months, timeStamp.getMonth())},
        { id: "currentYear", content: (timeStamp) => timeStamp.getFullYear()},
        { id: "currentDay", content: (timeStamp) => getName(days, timeStamp.getDay())},
    ]
    
    function updateDisplay2() {
        const timeStamp = new Date();
        calendarDisplay.forEach(element => {
            document.getElementById(element.id).textContent = element.content(timeStamp);
        })
    }
    
    updateDisplay2();
    setInterval(updateDisplay2, 1000);
    

    Print number every second

    printNumber(from, to)
    Output: a number every second starting from from ending with to
    function printNumbers(from, to) {
        let interId = setInterval(() => {
            console.log(from);
            from++;
            if (from > to) {
                clearInterval(interId);
            }
        }, 1000)
    };
    
    Result
    printNumbers(154647, 154651);
    
    154647
    154648
    154649
    154650
    154651
    

    "this"

    this context
    Code executed by setTimeout() is called from an execution context separate from the function from which setTimeout was called.
    this keyword mut be set in the call or with bind, ot it will default to the window (or global) object. It will not be the same as the this value for function that called setTimeout
    Also can't pass thisArg like in array methods like forEach() and reduce().
    this in usual situation
    const students = ['Abe', 'Ben', 'Che'];
    students.printName = function (el) {
      console.log(arguments.length > 0 ? this[el] : this);
    };
    
    students.printName();    // (3) ['Abe', 'Ben', 'Che', printName: ƒ]
    students.printName(0);   // Abe
    
    
    this in setTimeout
    setTimeout(students.printName, 1000);
    setTimeout(students.printName, 2000, 0);
    setTimeout(students.printName, 3000, "0");
    
    Results
    After 1 second:  object Window {window: Window, ...
    After 2 seconds: undefined
    After 3 seconds: undefined
    
    Because when it's called, this is not set, so the default (window) is used.
    Leave this for future work
    Continue reading: MDN

    Example: Promise timer

    About
    Timer with counter and cancel button. Promise is used to allow ringer alert appears after counter result.
    Why promise?
    In previous example, the ringer can't be delivered using alert box because in Javascript, there's a priority for alert box over HTML manipulation. Resulting in the last counter result didn't appear in the paragraph, despite counter coded prior to alert. To force alert to wait, we are going to use promise method.
    • Promise: setInterval is finished.
    • Then: alert can run.
    Methods featured
    • Promise
    • setTimeout
    • setInterval
    • clearTimeout
    • clearInterval
    Leave this for future work

    References