106

I'm writing some Javascript that interacts with library code that I don't own, and can't (reasonably) change. It creates Javascript timeouts used for showing the next question in a series of time-limited questions. This isn't real code because it is obfuscated beyond all hope. Here's what the library is doing:

....
// setup a timeout to go to the next question based on user-supplied time
var t = questionTime * 1000
test.currentTimeout = setTimeout( showNextQuestion(questions[i+1]), t );

I want to put a progress bar onscreen that fills towards questionTime * 1000 by interrogating the timer created by setTimeout. The only problem is, there seems to be no way to do this. Is there a getTimeout function that I'm missing? The only information on Javascript timeouts that I can find is related only to creation via setTimeout( function, time) and deletion via clearTimeout( id ).

I'm looking for a function that returns either the time remaining before a timeout fires, or the time elapsed after a timeout has been called. My progress bar code looks like this:

var  timeleft = getTimeout( test.currentTimeout ); // I don't know how to do this
var  $bar = $('.control .bar');
while ( timeleft > 1 ) {
    $bar.width(timeleft / test.defaultQuestionTime * 1000);
}

tl;dr: How do I find the time remaining before a javascript setTimeout()?


Here's the solution I'm using now. I went through the library section that's in charge of tests, and unscrambled the code (terrible, and against my permissions).

// setup a timeout to go to the next question based on user-supplied time
var t = questionTime * 1000
test.currentTimeout = mySetTimeout( showNextQuestion(questions[i+1]), t );

and here's my code:

// wrapper for setTimeout
function mySetTimeout( func, timeout ) {
    timeouts[ n = setTimeout( func, timeout ) ] = {
        start: new Date().getTime(),
        end: new Date().getTime() + timeout
        t: timeout
    }
    return n;
}

This works pretty spot-on in any browser that isn't IE 6. Even the original iPhone, where I expected things to get asynchronous.

14 Answers 14

70

Just for the record, there is a way to get the time left in node.js:

var timeout = setTimeout(function() {}, 3600 * 1000);

setInterval(function() {
    console.log('Time left: '+getTimeLeft(timeout)+'s');
}, 2000);

function getTimeLeft(timeout) {
    return Math.ceil((timeout._idleStart + timeout._idleTimeout - Date.now()) / 1000);
}

Prints:

$ node test.js 
Time left: 3599s
Time left: 3597s
Time left: 3595s
Time left: 3593s

This doesn't seem to work in firefox through, but since node.js is javascript, I thought this remark might be helpful for people looking for the node solution.

4
  • 5
    not sure if it's new version of node or what but in order for this to work i had to remove "getTime()" part. it seems _idleStart is already an integer now
    – kasztelan
    Commented Nov 16, 2013 at 22:42
  • 3
    I have just tried it in node 0.10, getTime() is removed, _idleStart is already the time
    – Tan Nguyen
    Commented Feb 11, 2014 at 16:49
  • 2
    not work on node 6.3, because the timeout structure is leak of those information.
    – Huan
    Commented Jul 15, 2016 at 4:04
  • 1
    will this work in vanila JS, not node?
    – CodeBy
    Commented Aug 30, 2022 at 10:30
64

EDIT: I actually think I made an even better one: https://stackoverflow.com/a/36389263/2378102

I wrote this function and I use it a lot:

function timer(callback, delay) {
    var id, started, remaining = delay, running

    this.start = function() {
        running = true
        started = new Date()
        id = setTimeout(callback, remaining)
    }

    this.pause = function() {
        running = false
        clearTimeout(id)
        remaining -= new Date() - started
    }

    this.getTimeLeft = function() {
        if (running) {
            this.pause()
            this.start()
        }

        return remaining
    }

    this.getStateRunning = function() {
        return running
    }

    this.start()
}

Make a timer:

a = new timer(function() {
    // What ever
}, 3000)

So if you want the time remaining just do:

a.getTimeLeft()
2
  • Thanks for that. Not exactly what I've been looking for, but function to count time left is very useful
    – instead
    Commented Apr 9, 2014 at 16:26
  • Just came across this as I was looking for something just like it. Having trouble adding a 'stop' ability though - like say there is a timer running and I want to initiate a complete stop/erase of the timer.
    – user756659
    Commented Sep 16, 2023 at 23:19
31

If you can't modify the library code, you'll need to redefine setTimeout to suit your purposes. Here's an example of what you could do:

(function () {
var nativeSetTimeout = window.setTimeout;

window.bindTimeout = function (listener, interval) {
    function setTimeout(code, delay) {
        var elapsed = 0,
            h;

        h = window.setInterval(function () {
                elapsed += interval;
                if (elapsed < delay) {
                    listener(delay - elapsed);
                } else {
                    window.clearInterval(h);
                }
            }, interval);
        return nativeSetTimeout(code, delay);
    }

    window.setTimeout = setTimeout;
    setTimeout._native = nativeSetTimeout;
};
}());
window.bindTimeout(function (t) {console.log(t + "ms remaining");}, 100);
window.setTimeout(function () {console.log("All done.");}, 1000);

This is not production code, but it should put you on the right track. Note that you can only bind one listener per timeout. I haven't done extensive testing with this, but it works in Firebug.

A more robust solution would use the same technique of wrapping setTimeout, but instead use a map from the returned timeoutId to listeners to handle multiple listeners per timeout. You might also consider wrapping clearTimeout so you can detach your listener if the timeout is cleared.

1
  • 16
    Due to execution time, this may drift by a few milliseconds over awhile. A more safe way to do this, is record the time when you started the timeout (new Date()) and then just subtract it from the current time (another new Date())
    – Jonathan
    Commented Apr 22, 2015 at 18:04
8

Server side Node.js specific

None of the above really worked for me, and after inspecting the timeout object it looked like everything was relative to when the process started. The following worked for me:

myTimer = setTimeout(function a(){console.log('Timer executed')},15000);

function getTimeLeft(timeout){
  console.log(Math.ceil((timeout._idleStart + timeout._idleTimeout)/1000 - process.uptime()));
}

setInterval(getTimeLeft,1000,myTimer);

Output:

14
...
3
2
1
Timer executed
-0
-1
...

node -v
v9.11.1

Edited output for brevity, but this basic function gives a approximate time until execution or since execution. As others mention, none of this will be exact due to the way node processes, but if I want to suppress a request that was run less than 1 minute ago, and I stored the timer, I don't see why this wouldn't work as a quick check. Could be interesting to juggle objects with refreshtimer in 10.2+.

1
  • All I get is: Cannot find name 'process'.
    – Eric
    Commented Apr 3, 2021 at 15:49
5

A quicker, easier way:

tmo = 1000;
start = performance.now();
setTimeout(function(){
    foo();
},tmo);

You can get the time remaining with:

 timeLeft = tmo - (performance.now() - start);
4

Javascript's event stacks don't operate how you would think.

When a timeout event is created, it is added to the event queue, but other events may take priority while that event is being fired, delay the execution time and postponing runtime.

Example: You create a timeout with a delay of 10 seconds to alert something to the screen. It will be added to the event stack and will be executed after all current events are fired (causing some delay). Then, when the timeout is processed, the browser still continues to capture other events add them to the stack, which causes further delays in the processing. If the user clicks, or does a lot of ctrl+typing, their events take priority over the current stack. Your 10 seconds can turn into 15 seconds, or longer.


That being said, there are many ways to fake how much time has passed. One way is to execute a setInterval right after you add the setTimeout to the stack.

Example: Perform a settimeout with a 10 second delay (store that delay in a global). Then perform a setInterval that runs every second to subtract 1 from the delay and output the delay remaining. Because of how the event stack can influence actual time (described above), this still won't be accurate, but does give a count.


In short, there is no real way to get the remaining time. There are only ways to try and convey an estimate to the user.

1

I stopped by here looking for this answer, but was overthinking my problem. If you are here because you just need to keep track of time while you're setTimeout is in progress, here's another way to do it:

    var focusTime = parseInt(msg.time) * 1000

    setTimeout(function() {
        alert('Nice Job Heres 5 Schrute bucks')
        clearInterval(timerInterval)
    }, focusTime)

    var timerInterval = setInterval(function(){
        focusTime -= 1000
        initTimer(focusTime / 1000)
    }, 1000);
1

You can modify setTimeout to store each timeout's end time in a map and create a function called getTimeout to get the time left for a timeout with a certain id.

This was super's solution, but I modified it to use slightly less memory

let getTimeout = (() => { // IIFE
    let _setTimeout = setTimeout, // Reference to the original setTimeout
        map = {}; // Map of all timeouts with their end times

    setTimeout = (callback, delay) => { // Modify setTimeout
        let id = _setTimeout(callback, delay); // Run the original, and store the id
        map[id] = Date.now() + delay; // Store the end time
        return id; // Return the id
    };

    return (id) => { // The actual getTimeout function
        // If there was no timeout with that id, return NaN, otherwise, return the time left clamped to 0
        return map[id] ? Math.max(map[id] - Date.now(), 0) : NaN;
    }
})();

Usage:

// go home in 4 seconds
let redirectTimeout = setTimeout(() => {
    window.location.href = "/index.html";
}, 4000);

// display the time left until the redirect
setInterval(() => {
    document.querySelector("#countdown").innerHTML = `Time left until redirect ${getTimeout(redirectTimeout)}`;
},1);

Here's a minified version of this getTimeout IIFE:

let getTimeout=(()=>{let t=setTimeout,e={};return setTimeout=((a,o)=>{let u=t(a,o);return e[u]=Date.now()+o,u}),t=>e[t]?Math.max(e[t]-Date.now(),0):NaN})();

I hope this is as useful to you as it was for me! :)

0

No, but you can have your own setTimeout/setInterval for animation in your function.

Say your question looks like this:

function myQuestion() {
  // animate the progress bar for 1 sec
  animate( "progressbar", 1000 );

  // do the question stuff
  // ...
}

And your animation will be handled by these 2 functions:

function interpolate( start, end, pos ) {
  return start + ( pos * (end - start) );
}

function animate( dom, interval, delay ) {

      interval = interval || 1000;
      delay    = delay    || 10;

  var start    = Number(new Date());

  if ( typeof dom === "string" ) {
    dom = document.getElementById( dom );
  }

  function step() {

    var now     = Number(new Date()),
        elapsed = now - start,
        pos     = elapsed / interval,
        value   = ~~interpolate( 0, 500, pos ); // 0-500px (progress bar)

    dom.style.width = value + "px";

    if ( elapsed < interval )
      setTimeout( step, delay );
  }

  setTimeout( step, delay );
}
2
  • well the difference can only be measured in ms, because the animation starts at the top of your function. I saw you use jQuery. You can use jquery.animate then.
    – gblazex
    Commented Jun 29, 2010 at 21:22
  • The best way is to try and see it for yourself. :)
    – gblazex
    Commented Jun 29, 2010 at 21:24
0

Question has already been answered but I will add my bit. It just occured to me.

Use setTimeout in recursion as follows:

var count = -1;

function beginTimer()
{
    console.log("Counting 20 seconds");
    count++;

    if(count <20)
    {
        console.log(20-count+"seconds left");
        setTimeout(beginTimer,2000);
    }
    else
    {
        endTimer();
    }
}

function endTimer()
{
    console.log("Time is finished");
}

I guess the code is self explanatory

0

Check this one:

class Timer {
  constructor(fun,delay) {
    this.timer=setTimeout(fun, delay)
    this.stamp=new Date()
  }
  get(){return ((this.timer._idleTimeout - (new Date-this.stamp))/1000) }
  clear(){return (this.stamp=null, clearTimeout(this.timer))}
}

Make a timer:

let smtg = new Timer(()=>{do()}, 3000})

Get remain:

smth.get()

Clear timeout

smth.clear()
0
    (function(){
        window.activeCountdowns = [];
        window.setCountdown = function (code, delay, callback, interval) {
            var timeout = delay;
            var timeoutId = setTimeout(function(){
                clearCountdown(timeoutId);
                return code();
            }, delay);
            window.activeCountdowns.push(timeoutId);
            setTimeout(function countdown(){
                var key = window.activeCountdowns.indexOf(timeoutId);
                if (key < 0) return;
                timeout -= interval;
                setTimeout(countdown, interval);
                return callback(timeout);
            }, interval);
            return timeoutId;
        };
        window.clearCountdown = function (timeoutId) {
            clearTimeout(timeoutId);
            var key = window.activeCountdowns.indexOf(timeoutId);
            if (key < 0) return;
            window.activeCountdowns.splice(key, 1);
        };
    })();

    //example
    var t = setCountdown(function () {
        console.log('done');
    }, 15000, function (i) {
        console.log(i / 1000);
    }, 1000);
0

For anyone in need of a hook, check this out - should be pretty self explanatory.

Note that elapsed is an internal state variable that if passed outside of the hook will be incorrect!

import { useEffect, useRef, useState } from 'react';

const useTimeout = (callback, duration, renderDuration = 5) => {
  const ref = useRef<any>(null);
  const [timeInfo, setTimeInfo] = useState<{
    start: number;
    elapsed: number;
    percentComplete: number;
  }>({
    start: null,
    elapsed: 0,
    percentComplete: 0
  });

  useEffect(() => {
    return () => {
      if (ref.current) {
        clearTimeout(ref.current);
        ref.current = null;
      }
    };
  }, []);

  useEffect(() => {
    setTimeout(() => {
      if (ref.current == null) return;
      setTimeInfo((prev) => {
        const elapsed = Date.now() - prev.start + prev.elapsed;

        if (ref.current == null) return prev;
        return {
          start: prev.start,
          elapsed: prev.elapsed,
          percentComplete: (elapsed / duration) * 100
        };
      });
    }, renderDuration);
  }, [timeInfo]);

  return {
    percentComplete: timeInfo.percentComplete,
    isTimerRunning: ref.current != null,
    startTimeout: () => {
      if (ref.current != null) return;
      setTimeInfo((prev) => ({ ...prev, start: Date.now() }));
      ref.current = setTimeout(callback, duration - timeInfo.elapsed);
    },
    stopTimeout: () => {
      if (ref.current) {
        clearTimeout(ref.current);
        ref.current = null;
      }
      setTimeInfo((prev) => {
        const elapsed = Date.now() - prev.start + prev.elapsed;
        return {
          start: prev.start,
          elapsed: elapsed,
          percentComplete: (elapsed / duration) * 100
        };
      });
    },
    resetTimeout: () => {
      if (ref.current) {
        ref.current = null;
        clearTimeout(ref.current);
      }
      setTimeInfo({ start: null, elapsed: 0, percentComplete: 0 });
    },
    restartTimeout: () => {
      if (ref.current) {
        ref.current = null;
        clearTimeout(ref.current);
      }
      setTimeInfo({ start: Date.now(), elapsed: 0, percentComplete: 0 });
      ref.current = setTimeout(callback, duration);
    }
  };
};

export default useTimeout;
1
  • Your answer could be improved with additional supporting information. Please edit to add further details, so that understanding your answer is easier. You can find more information on how to write good answers in the help center. Commented Feb 23, 2023 at 13:56
0

I created a class to allow you to pause and resume timeouts and intervals, hope it helps!

class CustomTimer {
  constructor() {
    this._initState();
  }

  start(callback, delay, isInterval = false) {
    if (
      typeof callback !== "function" ||
      typeof delay !== "number" ||
      delay <= 0
    ) {
      throw new Error("Invalid arguments provided to start method.");
    }
    this.stop(); // Clear any previous timer

    this._userCallback = callback;
    this._startTime = Date.now();
    this._endTime = this._startTime + delay;
    this._remaining = delay;

    if (isInterval) {
      this._interval = delay;
    }

    this._startTimer(delay);
  }

  pause() {
    if (!this._timerId || this._isPaused) return;

    this._isPaused = true;
    this._remaining -= Date.now() - this._startTime;

    this._clearTimer();
  }

  resume() {
    if (!this._isPaused) return;

    this._startTimer(this._remaining);

    this._isPaused = false;
    this._startTime = Date.now();
    this._endTime = this._startTime + this._remaining;
  }

  stop() {
    this._clearTimer();
    this._initState();
  }

  _initState() {
    this._startTime = null;
    this._endTime = null;
    this._remaining = null;
    this._interval = null;
    this._userCallback = null;
    this._timerId = null;
    this._isPaused = false;
  }

  _startTimer(delay) {
    // If it's an interval and it's paused, then on resume, we just use a setTimeout.
    if (this._interval && this._isPaused) {
      this._timerId = setTimeout(() => {
        this._tick();
        this._timerId = setInterval(this._tick, this._interval);
      }, delay);
    } else {
      const timerFn = this._interval ? setInterval : setTimeout;
      this._timerId = timerFn(this._tick, delay);
    }
  }

  _clearTimer() {
    if (this._timerId) {
      // clearInterval works for both interval and timeout
      clearInterval(this._timerId);
      this._timerId = null;
    }
  }

  // Using an arrow function here ensures 'this' retains the current instance's context
  // We could also use .bind(this) in the constructor
  // We need to do this because setInterval and setTimeout change the context of 'this'
  _tick = () => {
    if (this._isPaused) return;

    this._userCallback();

    if (this._interval) {
      this._remaining = this._interval;
      this._startTime = Date.now();
      this._endTime = this._startTime + this._remaining;
    } else {
      this.stop();
    }
  };

  get timerId() {
    return this._timerId;
  }

  get timeRemaining() {
    if (this._isPaused) return this._remaining;
    return Math.max(this._endTime - Date.now(), 0);
  }
}

// Example usage

const timer = new CustomTimer();

timer.start(
  () => {
    console.log("Hello!");
  },
  5000,
  true
); // Execute the callback every 5 seconds

setTimeout(() => {
  timer.pause();
  console.log("Timer paused");
}, 2000); // Pause the timer after 2 seconds

setTimeout(() => {
  timer.resume(); // Resume the timer after 4 seconds (2 seconds after it was paused)
}, 4000);

setInterval(() => {
  console.log(`Time remaining: ${timer.timeRemaining}ms.`); // Get the time remaining every second
}, 1000);

Not the answer you're looking for? Browse other questions tagged or ask your own question.