I’m trying to use setTimeout in my JavaScript code to delay some actions, but the function either runs immediately or doesn’t fire at all. I’m not sure if I’m passing the callback or the delay correctly, and I’m confused about how the timing and scope work. Can someone explain the right way to use setTimeout with examples, and point out common mistakes that might cause it to fail?
You are tripping on the classic setTimeout gotchas. Here is the core stuff you need.
- Basic usage
setTimeout takes:
- A function (callback)
- A delay in milliseconds
- Optional extra arguments
Correct:
setTimeout(function () {
console.log(‘Runs after 1 second’);
}, 1000);
Arrow version:
setTimeout(() => {
console.log(‘Runs after 1 second’);
}, 1000);
Wrong:
setTimeout(doSomething(), 1000); // This runs doSomething immediately
You must pass the function, not the result of calling it.
Correct:
setTimeout(doSomething, 1000);
function doSomething() {
console.log(‘Delayed’);
}
Or, if you need arguments:
setTimeout(() => doSomething(123, ‘hi’), 1000);
- Why it runs immediately
This line:
setTimeout(myFunc(), 1000);
Calls myFunc right away, then passes its return value to setTimeout. If myFunc returns nothing, you send undefined as the callback. Then nothing happens at the timeout.
You need:
setTimeout(myFunc, 1000);
Or:
setTimeout(function () {
myFunc();
}, 1000);
- Why it never fires
Common causes:
- Delay is not a number:
setTimeout(doStuff, ‘abc’); // delay becomes 0
- Code errors before the timeout
- The page or tab is closed or background throttling kicks in
- You cleared it:
const id = setTimeout(doStuff, 1000);
clearTimeout(id);
- Passing arguments
Two common patterns:
A) Extra args to setTimeout:
function greet(name) {
console.log('Hi ’ + name);
}
setTimeout(greet, 1000, ‘Alex’);
B) Wrap in an arrow:
setTimeout(() => greet(‘Alex’), 1000);
- With this and methods
If you use a method that depends on this:
const obj = {
name: ‘Test’,
sayHi() {
console.log('Hi ’ + this.name);
}
};
setTimeout(obj.sayHi, 1000); // this is lost
Fix with bind:
setTimeout(obj.sayHi.bind(obj), 1000);
Or wrap:
setTimeout(() => obj.sayHi(), 1000);
- Loops and setTimeout
Old trap:
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
Outputs 3, 3, 3.
Use let:
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
Or an IIFE:
for (var i = 0; i < 3; i++) {
((iCopy) => {
setTimeout(() => {
console.log(iCopy);
}, 1000);
})(i);
}
- setTimeout vs setInterval
setTimeout runs once.
setInterval repeats:
const id = setInterval(() => {
console.log(‘Tick’);
}, 1000);
// later
clearInterval(id);
You can chain setTimeout for more control:
function tick() {
console.log(‘Tick’);
setTimeout(tick, 1000);
}
setTimeout(tick, 1000);
- Quick checklist for your bug
- Check you pass a function, not a function call
- Check delay is a number, in milliseconds
- Check you do not overwrite or clear the timeout
- Wrap calls with arguments in an arrow or function
- If using this, bind or wrap the method
If you paste the exact snippet where it runs immediately or not at all, people can point at the exact line that is off.
Your issue is almost always one of these three: how you pass the function, where the code runs, or what you expect setTimeout to do.
@sternenwanderer already hit the basics of “don’t do myFunc() inside setTimeout”. I’ll focus on the other kinds of confusion you hinted at.
1. setTimeout is asynchronous, not a “pause”
A super common mental trap: expecting this
console.log('A');
setTimeout(() => {
console.log('B');
}, 1000);
console.log('C');
to log A, then 1 second pause, then B, then C.
What actually happens:
A
C
...1 second...
B
Because JavaScript does not stop and wait. setTimeout just schedules the callback to run later on the event loop. If your code “seems to run immediately,” sometimes it’s just that the rest of the script continues right away and you expected a blocking delay.
If you want something to happen strictly after something else, you must put that “something else” inside the callback or chain with Promises / async/await:
setTimeout(() => {
doThing();
doNextThing();
}, 1000);
or
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function run() {
console.log('before');
await delay(1000);
console.log('after 1s');
}
2. Delay is a minimum, not guaranteed timing
Another subtle one: setTimeout(fn, 1000) means “run fn after at least 1000 ms, when the call stack is empty”, not “exactly 1 second.”
If you block the main thread:
setTimeout(() => console.log('timeout'), 1000);
// long blocking loop
const start = Date.now();
while (Date.now() - start < 3000) {
// doing nothing, just blocking
}
The callback will not fire until that monstrous loop ends, so it looks like “it never fired” or fired way too late.
So if your code is CPU heavy or stuck in a loop, setTimeout callbacks can be delayed a lot.
3. Scope & where the callback lives
Another thing that can make you think it “never fires” is a reference problem:
{
function myFunc() {
console.log('hi');
}
}
setTimeout(myFunc, 1000); // Error in strict modules or bundlers
Or with classes:
class Thing {
#secret = 42;
start() {
setTimeout(this.logSecret, 1000);
}
logSecret() {
console.log(this.#secret); // this is undefined here, boom
}
}
The timeout does fire, but you get an error when it tries to run the method. It can look like “nothing happend”.
Use a wrapper or bind:
setTimeout(() => this.logSecret(), 1000);
// or
setTimeout(this.logSecret.bind(this), 1000);
If you’re using modules, bundlers, or frameworks, pay attention to where the function is defined and if it’s actually in scope when you pass it to setTimeout.
4. Confusion with setTimeout inside logic flow
Another classic pattern that misbehaves:
function doStuff() {
if (someCond) {
setTimeout(handleThing(), 1000);
}
}
Here, because of the (), handleThing runs immediately, often changing someCond or other state, and then the timeout silently stores whatever that function returned.
What you probably wanted:
function doStuff() {
if (someCond) {
setTimeout(handleThing, 1000);
}
}
Or, if you need parameters:
setTimeout(() => handleThing(param1, param2), 1000);
This is just reiterating what was said, but people often slip specifically inside conditionals or ternaries, not in simple examples.
5. Delays smaller than you think, or clamped
In browsers:
- Delays less than 4 ms are often clamped up to 4 ms.
- Background tabs can have timeouts clamped to 1000 ms or more.
- Some environments throttle like crazy to save battery.
So code like:
setTimeout(fn, 0);
does not mean “immediately”. It means “after the current stack, with a minimum 4 ms or more in practice.” That affects debugging when you think “it should fire instantly” and you step through.
6. Make debugging the timeout itself easy
Instead of guessing if it fired, log straight from the callback:
setTimeout(() => {
console.log('timeout fired');
doWork();
}, 1000);
If you don’t see the log:
- The line with setTimeout never executed.
- It executed but threw an error before it.
- You cleared the timeout somehow.
- The environment killed / throttled / reloaded.
Throwing both logs helps:
console.log('about to schedule timeout');
setTimeout(() => {
console.log('timeout fired');
}, 1000);
7. Quick mental model to avoid most bugs
When in doubt, read your code like this:
setTimeout(fn, X)→ “Putfnon a todo list to run later, after at least X ms, when everything else is done.”- Inside that callback, you’re in a fresh turn of the event loop. Variables captured by closures still exist, but any synchronous code that follows the original setTimeout line has already run.
So if your code:
- Calls
setTimeout(someFunc(), 1000) - Expects the script to pause
- Relies on
thisbeing the same inside the callback as outside - Or blocks the main thread with heavy loops
you’re going to get the exact “runs immediately or never” vibe you described.
If you paste your exact snippet, you’ll probably find it’s one of:
myFunc()instead ofmyFunc- method losing
this - heavy blocking code before the timeout
- expecting setTimeout to act like
sleepinstead of “schedule and continue”
@sternenwanderer covered the signatures and classic gotchas nicely; the big missing piece is usually just accepting that setTimeout is “schedule & forget”, not “pause and wait”, and treating it as such in your control flow.