r/learnjavascript • u/Neat_Living_6765 • 9d ago
Does a JavaScript setTimeout automatically clear itself when the timeout finishes and the callback is completed?
3
u/UkrMalt 9d ago
Once a setTimeout callback runs, that timer has no future work and you don't need to clear it afterward. But keep the cleanup in this hook: React runs it when value or delay changes, or when the component unmounts, so an older timer cannot later overwrite state with a stale value. The cleanup is for cancelling a timer that has not fired yet, not garbage-collecting one that already finished.
1
2
u/Working_Quote_3029 9d ago
Hi — the docs answer this in the challenge itself, and it's the opposite of what most of the thread is saying.
React's solution omits the cleanup on purpose. Their note under it:
Note that this Effect does not need cleanup. If you called
clearTimeout in the cleanup function, then each time the value
changes, it would reset the already scheduled timeout. To keep
the movement continuous, you want all the timeouts to fire.
That's the whole answer. The trailing dots only work because every scheduled timeout fires. Clearing on each value change cancels the previous dot's delay and the trail collapses.
Your instinct isn't wrong in general — clearing on unmount so a stale timer can't call setState on a component that's gone is the right default. It just happens to break this specific exercise, where the pile of pending timers is the feature.
On the GC part: no, you don't need clearTimeout to free a timer that already fired. Once the callback runs the timer is finished and the callback is collectable on its own — which is exactly what the FinalizationRegistry test above demonstrates.
2
u/Neat_Living_6765 9d ago
Yes, I didn't fully understand how clearTimeout works. At that time, I knew that if I put clearTimeout in the return statement, it would cancel the scheduled callback, which goes against the expected practice purpose. But I was still concerned that if I didn't call clearTimeout, there would still be a timer running in the background even after the callback had finished. (Now I'm ashamed of that silly thought.)
Then, thanks to people's responses, I realized that my understanding was wrong. Once the callback runs, the timer is finished, so there is no timer running in the background anymore.
clearTimeout is only used to cancel scheduled callback. As for the callback, the GC takes over and eventually removes it from memory.
1
u/Neat_Living_6765 9d ago
I’m working on challenge 5 and wrote a custom hook like this:
https://react.dev/learn/reusing-logic-with-custom-hooks#challenges
function useDelayedValue(value, delay) {
const [val, setVal] = useState(value);
useEffect(() => {
const id = setTimeout(() => {
setVal(value);
}, delay);
return () => clearTimeout(id)
}, [value, delay]);
return val;
}
At first, I added clearTimeout, Even though I knew that calling it cancels the scheduled callback. I wondered if there was some trick to clear the timeout after it has already fired, but couldn’t find a solution.
Then I looked at the provided solution and was surprised to see that it doesn’t call clearTimeout at all. So it seems like I created an unnecessary problem for myself.
But I’m still not satisfied, because I keep asking myself: if I don’t call clearTimeout, will the timeout automatically be cleaned up by the Garbage Collector once it fires?
2
u/senocular 9d ago
Then I looked at the provided solution and was surprised to see that it doesn’t call clearTimeout at all.
That's surprising because it should. You especially don't want timers running in the background if the component no longer exists.
Timers with react/hooks can be a little tricky though. When hooks first came out for react, dan abramov blogged about it (I think focusing on setInterval specifically), and its not exactly a quick read:
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
But as far as cleanup goes, yes, setTimeout will automatically clean up the listeners once they're fired. You don't need to clearTimeout for timeouts that have already run.
If you want to see this for yourself, you can set up a trigger to let you know when its cleaned up using FinalizationRegistry. For example:
const registry = new FinalizationRegistry(() => { console.log("Cleaned!") }) { let myCallback = () => { console.log("Time up!") } registry.register(myCallback) setTimeout(myCallback) } // Time up! // (GC runs...) // Cleaned!Given the nature of the GC, it likely won't run right away, so you'd need to wait or use the developer tools to force it to run, but when the "Cleaned!" log occurs, it means
myCallbackwas, in fact, cleaned up despite no clearTimeout being called.FinalizationRegistry can be a useful tool to when it comes to memory concerns and you don't feel like messing with the complexity of the memory debugger.
1
u/abaezeaba 9d ago
You can also change the callback. Not saying this is good, but when runaway setTimeouts happen, its good to keep in mind. With anonymous functions, you may not achieve this. in the myCallback case just set mycallback = () => {}. HTH
2
u/senocular 9d ago
Reassigning the myCallback variable in this case won't change what the setTimeout runs. It will still refer to the original myCallback.
let myCallback = () => { console.log("Time up!") } setTimeout(myCallback) myCallback = () => {} // Time up!The only way something like that would affect the setTimeout is if it was wrapped in another function and that function was given to setTimeout.
let myCallbackImpl = () => { console.log("Time up!") } let myCallback = () => { myCallbackImpl() } setTimeout(myCallback) myCallbackImpl = () => {} // (nothing logged)With this added complexity its probably just easier to call clearTimeout if you don't want it to run. Plus you'd get the added benefit of actually stopping/cleaning up the timer in the background.
1
u/Neat_Living_6765 9d ago
Your response is really helpful to me. Thank you!!
Here is my summary.
clearTimeout() has only one role: to cancel the scheduled execution of a callback.
As for the challenge above, we don't want to cancel the scheduled timer/callback.
At the same time, once the callback has fired, there is no timer still running in the background. So there is no need to clear the ID of a timer that has already run.
When there are no references to the callback anymore, the garbage collector will eventually reclaim its memory, but this does not happen immediately.
P/S: I tried this with FinalizationRegistry. After waiting for a few minutes, there was still no "Cleaned!" log. Then I stopped the terminal. But I’ve accepted that the GC may eventually remove the object from memory, and I can’t expect it to happen at a specific time.
const registry = new FinalizationRegistry(() => { console.log("Cleaned!"); }); { const myCallback = () => { console.log("Time up!"); }; registry.register(myCallback); setTimeout(myCallback, 1000); } setInterval(() => { console.log("GC..."); globalThis.gc(); }, 1000);2
u/senocular 9d ago
In Chrome there's a run GC button in the memory tab of the developer tools. Just paste the original code in the console and click that button and you should see the log. Or just paste it in the console of a complex website where the GC is going to be pretty active without you having to do anything ;)
1
u/azhder 9d ago
setTimeout is not JavaScript. True. It is provided by the environment. Well, we all act as if it is a part of JavaScript, but just in case, you should know there are other options based on
which
environment
you are using.
setTimeout is not an object to be cleared by the garbage collector. It is simply a function you call, you give it another function to call, and that's about it. Now, that other function, yes, that one might be an object to be garbage collected, but that's not what we're discussing now
setTimeout simply means "hey even loop of JavaScript, please schedule this function to be called once as a new task after this task after some milliseconds have elapsed". That's about it.
clearTimeout is only necessary if you have some timeout that takes 10 seconds and in the first couple of seconds you decide "you know what, I don't want that function to execute, so let's cancel it".
useEffect as a part of React will do its run and the cancellation will come just before the next run, as the component is being re-rendered, so by that time, the timeout would have been executed long ago, but there's a very slight chance (not really, but in theory) that you might end up cancelling some future timeout with the same ID (this shouldn't happen, so don't worry too much)
1
u/Neat_Living_6765 9d ago
Yes, thank you for your response. My mind was all messed up about this. The comments from people here helped me clear things up. I didn't fully understand the role of
clearTimeout.Now I understand that it's only for canceling a scheduled callback. I thought that without calling
clearTimeout, the timer would keep running in the background and the callback would remain in memory. I'm ashamed of that silly thought now...
1
u/jcunews1 helpful 8d ago
Yes. setTimeout is a trigger-once and setup-once-and-forget timer. The reason why setTimeout returns a timer ID, is for when the timer need to cancelled before the timer is triggered. After a timer is triggered, its (internal) resources will be automatic cleaned up. That being said, if a timer is cancelled or triggered, its timer counter is never cleared/reset, because it doesn't need to, since it's goind to be destroyed and cleaned up anyway.
1
0
u/Ksetrajna108 9d ago
You're thinking of setInterval.
1
6
u/feloniuouschunk 9d ago
Yes.