Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

setTimeout e setInterval em JavaScript

O atraso que você pede é o mínimo, nunca o exato. Como agendar, cancelar e medir timers em JavaScript sem vazar intervalo nem congelar a interface.

Rodolfo Mori7 min de leitura

setTimeout agenda uma função para rodar uma vez, depois de um atraso. setInterval agenda para rodar repetidamente, a cada intervalo. Os dois devolvem um identificador que serve para cancelar — clearTimeout e clearInterval.

O que quase nenhum tutorial diz na primeira linha: o número que você passa é o atraso mínimo, não o exato. O timer entra na fila; ele só roda quando a thread estiver livre. Tudo neste texto sai dessa frase.

Timer é despertador, não compromisso de agenda

Quando você programa um despertador para daqui a dez minutos, ele fica pronto para tocar naquele horário. Mas, se o alto-falante estiver ocupado, o som sai assim que houver espaço. O timer do JavaScript funciona com a mesma ideia: depois do atraso, a função entra na fila; quem decide quando ela realmente roda é a disponibilidade da thread.

O nome técnico do número é delay, o atraso mínimo em milissegundos. Ele não é uma promessa de pontualidade. Para sentir a diferença, tente prever a ordem de três mensagens: uma impressa agora, outra num setTimeout(..., 0) e uma conta longa entre as duas. Depois execute. Esse pequeno teste vale mais que decorar “zero milissegundos”, porque mostra que zero ainda significa “assim que a fila puder”.

Agendar: função, atraso e argumentos extras

O primeiro argumento é a função — sem parênteses. O segundo é o atraso em milissegundos. Do terceiro em diante, os argumentos que você quer entregar para a função na hora da chamada.

js
console.log('pedido criado');

setTimeout(() => {
  console.log('e-mail de confirmação enviado');
}, 1000);

setTimeout((cliente, valor) => {
  console.log(`aviso de separação: ${cliente}, R$ ${valor.toFixed(2)}`);
}, 500, 'Ana', 289.9);

console.log('resposta devolvida para o navegador');
pedido criado resposta devolvida para o navegador aviso de separação: Ana, R$ 289.90 e-mail de confirmação enviado

As duas linhas síncronas saíram primeiro, e os agendamentos saíram na ordem do atraso — não na ordem em que foram escritos. O programa não parou em nenhum momento para esperar.

Cancelar antes de disparar

O retorno do setTimeout é o identificador. No navegador é um número; no Node é um objeto Timeout:

js
const id = setTimeout(() => console.log('carrinho abandonado: cupom enviado'), 500);

console.log('id do timer:', id.constructor.name);

// o cliente finalizou a compra antes dos 500ms
clearTimeout(id);
console.log('compra finalizada — timer cancelado');
id do timer: Timeout compra finalizada — timer cancelado

O cupom nunca foi enviado. Esse padrão — agendar e cancelar se o usuário agir antes — é o coração do debounce de campo de busca.

O atraso é o mínimo, e dá para medir

Peço um timer de 100ms e coloco uma recontagem de estoque síncrona logo abaixo:

js
const inicio = Date.now();

setTimeout(() => {
  console.log('timer de 100ms disparou aos', Date.now() - inicio, 'ms');
}, 100);

// recontagem síncrona do estoque
let peso = 0;
for (let i = 0; i < 900_000_000; i++) peso += i % 7;

console.log('recontagem terminou aos', Date.now() - inicio, 'ms');
recontagem terminou aos 530 ms timer de 100ms disparou aos 532 ms

432ms de atraso sobre o que foi pedido, e ninguém errou nada: o timer ficou pronto aos 100ms e esperou a pilha desocupar. Esse é o mesmo mecanismo descrito em JavaScript assíncrono — vale a leitura se a fila e a pilha ainda parecerem abstratas.

O clamp de 4ms — e o que o Node faz de diferente

A especificação HTML manda o navegador travar o atraso mínimo em 4ms quando o timer está aninhado a mais de cinco níveis (um setTimeout agendado dentro de outro, e assim por diante). É por isso que uma animação feita com setTimeout(f, 0) recursivo trava em torno de 250 quadros por segundo no navegador.

O Node não aplica essa regra. Medindo oito níveis de aninhamento:

js
let nivel = 0;
let anterior = Date.now();

function proxima() {
  const agora = Date.now();
  console.log(`nivel ${nivel}: ${agora - anterior}ms desde o anterior`);
  anterior = agora;
  nivel += 1;
  if (nivel < 8) setTimeout(proxima, 0);
}

setTimeout(proxima, 0);
nivel 0: 1ms desde o anterior nivel 1: 2ms desde o anterior nivel 2: 1ms desde o anterior nivel 3: 2ms desde o anterior nivel 4: 1ms desde o anterior nivel 5: 1ms desde o anterior nivel 6: 1ms desde o anterior nivel 7: 2ms desde o anterior

Nenhum salto para 4ms a partir do quinto nível: no Node o mínimo é 1ms e fica nisso. Ou seja, o mesmo código de agendamento roda com ritmos diferentes no servidor e no navegador — e a medição acima é a razão para não calibrar animação por contagem de disparos.

setInterval: o clearInterval que ninguém escreve

Um contador que se encerra sozinho precisa guardar o id e chamar clearInterval dentro do próprio callback:

js
let restante = 3;

const id = setInterval(() => {
  console.log(`carrinho expira em ${restante} minutos`);
  restante -= 1;
  if (restante === 0) {
    clearInterval(id);
    console.log('carrinho liberado');
  }
}, 300);
carrinho expira em 3 minutos carrinho expira em 2 minutos carrinho expira em 1 minutos carrinho liberado

Tire o clearInterval e o mesmo código não para nunca. Deixei rodando dois segundos antes de matar o processo à mão:

js
let restante = 3;

setInterval(() => {
  console.log(`carrinho expira em ${restante} minutos`);
  restante -= 1;
}, 300);
carrinho expira em 3 minutos carrinho expira em 2 minutos carrinho expira em 1 minutos carrinho expira em 0 minutos carrinho expira em -1 minutos carrinho expira em -2 minutos

Num script de terminal isso é chato. Num componente de interface é vazamento: o componente sai da tela, o intervalo continua vivo, continua mexendo em estado que não existe mais — e cada nova montagem cria mais um. Todo setInterval precisa ter um lugar definido onde é cancelado, escrito no mesmo momento em que o intervalo é criado.

O intervalo não desconta o tempo do seu código

O setInterval não dispara duas execuções ao mesmo tempo. Se o callback demora mais que o intervalo, o período real vira o tempo do callback. Aqui o intervalo é de 100ms e a recontagem leva bem mais que isso:

js
const inicio = Date.now();
let volta = 0;

const id = setInterval(() => {
  volta += 1;
  const entrou = Date.now() - inicio;
  // recontagem de estoque, síncrona e pesada
  let n = 0;
  for (let i = 0; i < 400_000_000; i++) n += 1;
  console.log(`volta ${volta}: entrou aos ${entrou}ms, saiu aos ${Date.now() - inicio}ms`);
  if (volta === 4) clearInterval(id);
}, 100);
volta 1: entrou aos 101ms, saiu aos 252ms volta 2: entrou aos 253ms, saiu aos 434ms volta 3: entrou aos 435ms, saiu aos 584ms volta 4: entrou aos 584ms, saiu aos 722ms

Quatro voltas de um intervalo de 100ms deveriam terminar em 400ms; terminaram em 722ms. E repare que a volta 2 entrou 1ms depois de a volta 1 sair: o intervalo virou “assim que der”. Se o seu código precisa de ritmo confiável, meça o tempo decorrido com Date.now() dentro do callback em vez de multiplicar o número de disparos pelo intervalo.

O erro clássico: chamar a função em vez de passar

Um par de parênteses a mais muda tudo. Em vez de entregar a função, você entrega o retorno dela:

js
function avisarSeparacao() {
  console.log('separação liberada');
}

console.log('pedido pago');
setTimeout(avisarSeparacao(), 2000);
console.log('fim do handler');
pedido pago separação liberada node:timers:116 validateFunction(callback, 'callback'); ^ TypeError [ERR_INVALID_ARG_TYPE]: The "callback" argument must be of type function. Received undefined at setTimeout (node:timers:116:3) at file:///private/tmp/loja-async/timer-parenteses.mjs:6:1 Node.js v24.16.0

Leia a saída na ordem: separação liberada apareceu antes de fim do handler e sem esperar dois segundos — a função rodou na hora. Depois o setTimeout recebeu undefined (o retorno dela) e reclamou. No navegador não há erro nenhum: o setTimeout engole o valor inválido em silêncio, e você fica com uma função que executou cedo e um agendamento que nunca acontece.

As duas formas corretas de passar argumentos:

js
function avisarSeparacao(pedido) {
  console.log(`separação liberada para o pedido ${pedido}`);
}

console.log('pedido pago');
setTimeout(() => avisarSeparacao(8241), 300);
setTimeout(avisarSeparacao, 300, 8242);
console.log('fim do handler');
pedido pago fim do handler separação liberada para o pedido 8241 separação liberada para o pedido 8242

Timer em aba inativa

Os navegadores limitam o que roda numa aba em segundo plano: timers passam a disparar no máximo uma vez por segundo, e depois de alguns minutos a economia fica mais agressiva ainda. É uma regra da plataforma, não um defeito do seu código — e ela quebra qualquer contagem de tempo feita somando disparos.

você quer não faça faça
contar tempo somar disparos de setInterval comparar dois Date.now()
animar setTimeout recursivo requestAnimationFrame
esperar dado da API setTimeout “generoso” await na Promise da requisição
repetir tarefa pesada setInterval fixo reagendar com setTimeout no fim do trabalho

A última linha da tabela é a mais útil: reagendar com setTimeout no fim do próprio callback garante o intervalo entre execuções, sem nunca acumular atraso nem empilhar trabalho.

Antes de escrever o próximo timer

  • O atraso é mínimo. Se o número precisa ser exato, timer é a ferramenta errada.
  • Todo setInterval nasce com o clearInterval já planejado.
  • Função sem parênteses no primeiro argumento. Argumentos extras vão do terceiro em diante, ou dentro de uma arrow.
  • Timer não serve para esperar rede. Para isso existe Promise — assunto da próxima lição, onde a espera vira um valor que você pode passar adiante em vez de um palpite em milissegundos.

O mapa completo dos assuntos está no guia de JavaScript.

  • settimeout
  • setinterval
  • timers
  • assincrono
  • clearinterval

Perguntas frequentes

setTimeout de 0ms roda imediatamente?
Não. Ele roda depois que a pilha de chamadas esvaziar e depois que a fila de microtasks for drenada. Em código pesado, esse "0ms" vira facilmente centenas de milissegundos, e é fácil medir isso com Date.now.
Preciso guardar o retorno do setTimeout?
Só se você puder precisar cancelar. O retorno é o identificador que clearTimeout e clearInterval usam. Em setInterval, guardar é praticamente obrigatório, porque sem o id não há como parar o intervalo.
setInterval garante que minha função rode a cada N milissegundos?
Não. Ele garante que duas execuções não se sobreponham e tenta manter o ritmo, mas se o seu callback demora mais que o intervalo, o período real passa a ser o tempo do callback. Medir é a única forma de saber.
Por que meu timer fica mais lento quando troco de aba?
Os navegadores limitam timers em abas em segundo plano a no mínimo um disparo por segundo, para economizar bateria. Contagem de tempo baseada em contar disparos vai atrasar; use a diferença entre dois Date.now.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MDN — setTimeout() — developer.mozilla.org
  2. HTML Standard — Timers — html.spec.whatwg.org

Continue por aqui