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

Função callback em JavaScript: o que é e exemplos

Callback é a função que você passa sem os parênteses para outra chamar depois. De forEach a setTimeout, com o erro dos parênteses a mais reproduzido no Node.

Rodolfo Mori6 min de leitura

Callback é uma função que você passa como argumento para outra função, que decide quando chamá-la. A regra que resolve 90% dos bugs de iniciante com callback cabe numa linha: passe o nome da função, sem os parênteses. Com parênteses você executa na hora e entrega o resultado; sem parênteses você entrega a função.

Isso é possível porque em JavaScript função é um valor, como número e string. Ela pode ser guardada numa variável, colocada num array e passada adiante — é a mesma propriedade que permite as function expressions da lição anterior.

Callback está em todo lugar: forEach, map, filter, sort, setTimeout, addEventListener, leitura de arquivo. Você já usou dezenas antes de saber o nome.

O callback é o comportamento entregue para depois. Essa tradução ajuda, mas a gente continua usando o nome correto porque é ele que aparece na documentação, nas APIs e nas mensagens de erro.

Entregar a receita, não o prato pronto

Se uma cozinha pede a receita para preparar o prato quando chegar um pedido, entregar um prato já pronto resolve outro problema. A referência da função é a receita: pode ser chamada no momento escolhido por map, setTimeout ou addEventListener. O retorno da chamada é o prato que já ficou pronto. Callback é a primeira coisa, não a segunda.

Antes de rodar o exemplo, marque qual função recebe outra e quem decide o momento da chamada. Depois acompanhe o log dentro e fora do callback e compare a ordem. Assim você verifica, sem decorar API, quem controla a execução.

A função orcar não sabe calcular frete. Ela recebe quem sabe:

js
function freteExpresso(peso) {
  return 24.9 + peso * 3;
}

function fretePac(peso) {
  return 12.5 + peso * 1.2;
}

function orcar(peso, calculadora) {
  return `R$ ${calculadora(peso).toFixed(2)}`;
}

console.log(orcar(2, freteExpresso));
console.log(orcar(2, fretePac));
console.log(orcar(2, (p) => 39.9 + p));
R$ 30.90 R$ 14.90 R$ 41.90

Três chamadas, três regras de frete, uma função orcar só. O parâmetro calculadora é o callback; freteExpresso, fretePac e a arrow anônima são os valores que ele recebeu. Note que orcar não faz ideia de qual chegou — ela só sabe que dá para pôr parênteses depois.

Esse desenho tem nome: inversão de controle. Quem escreve orcar decide o esqueleto; quem chama decide o detalhe. É a mesma ideia por trás de map, filter e sort, todos implementados assim por dentro.

Os métodos de array são callbacks o tempo todo

js
const itens = [
  { nome: 'Teclado mecânico', preco: 289.9 },
  { nome: 'Cadeira gamer', preco: 1199 },
  { nome: 'Mouse sem fio', preco: 149.9 },
];

itens.forEach((item, indice) => console.log(`${indice + 1}. ${item.nome}`));

console.log(itens.map((item) => item.nome));
console.log(itens.filter((item) => item.preco < 300).length, 'itens abaixo de 300');
console.log([...itens].sort((a, b) => a.preco - b.preco).map((i) => i.preco));
1. Teclado mecânico 2. Cadeira gamer 3. Mouse sem fio [ 'Teclado mecânico', 'Cadeira gamer', 'Mouse sem fio' ] 2 itens abaixo de 300 [ 149.9, 289.9, 1199 ]

Repare que quem chama o seu callback é o forEach, e é ele que decide os argumentos: item, índice e o array inteiro, nessa ordem. Você recebe os que quiser — declarar menos parâmetros do que o esperado é permitido, como mostra a lição de parâmetros e retorno.

O sort é o único da lista que muta o array original, por isso a cópia com [...itens]. E o callback dele não devolve verdadeiro ou falso: devolve um número negativo, zero ou positivo.

setTimeout: o callback que roda depois

Aqui aparece a característica que separa callback síncrono de assíncrono. A função entra numa fila e só é chamada quando a linha principal termina.

js
console.log('1. pedido criado');

setTimeout(function confirmarPagamento() {
  console.log('3. pagamento confirmado');
}, 50);

console.log('2. e-mail de confirmação enfileirado');
1. pedido criado 2. e-mail de confirmação enfileirado 3. pagamento confirmado

Os números da saída não estão na ordem em que aparecem no arquivo — estão na ordem em que o motor executou. Mesmo com 0 no lugar de 50, o resultado seria o mesmo: setTimeout agenda para depois do código atual, não para “agora mesmo”.

Repare também que dei um nome à função passada: confirmarPagamento. Ela não precisava de nome, mas quando algo estourar lá dentro, o rastro vai dizer at confirmarPagamento em vez de at <anonymous>.

O erro no navegador: addEventListener com parênteses

Este é o clássico. O botão é registrado e a página parece morta — ou pior, a ação acontece antes de qualquer clique.

html
<button id="finalizar">Finalizar compra</button>
<script type="module">
  const botao = document.querySelector('#finalizar');

  function finalizarCompra() {
    console.log('pedido enviado');
  }

  botao.addEventListener('click', finalizarCompra()); // ERRADO
  botao.addEventListener('click', finalizarCompra); //  certo
</script>

O EventTarget do navegador também existe no Node, então dá para reproduzir o comportamento no terminal:

js
const botaoFinalizar = new EventTarget();

function finalizarCompra() {
  console.log('pedido enviado');
}

botaoFinalizar.addEventListener('click', finalizarCompra());
botaoFinalizar.dispatchEvent(new Event('click'));
pedido enviado (node:3375) AddEventListenerArgumentTypeWarning: addEventListener called with undefined which has no effect. (Use `node --trace-warnings ...` to show where the warning was created)

Leia a saída na ordem. pedido enviado apareceu antes de qualquer clique — foi a chamada com parênteses, executada na hora do registro. O que ficou registrado como listener foi o retorno dela, undefined. Depois, o dispatchEvent disparou o evento e nada aconteceu.

O Node avisa; o navegador não diz absolutamente nada. Corrigindo:

js
const botaoFinalizar = new EventTarget();

function finalizarCompra(evento) {
  console.log('pedido enviado no evento', evento.type);
}

botaoFinalizar.addEventListener('click', finalizarCompra);
botaoFinalizar.dispatchEvent(new Event('click'));
pedido enviado no evento click

Erro primeiro: a convenção do Node

Antes das Promises, toda API assíncrona do Node seguia a mesma assinatura de callback: o primeiro parâmetro é o erro, o segundo é o resultado.

js
import { readFile } from 'node:fs';

function carregarPedido(arquivo, pronto) {
  readFile(arquivo, 'utf8', (erro, conteudo) => {
    if (erro) return pronto(erro);
    pronto(null, JSON.parse(conteudo));
  });
}

carregarPedido('/private/tmp/loja/pedido-7412.json', (erro, pedido) => {
  if (erro) return console.log('falhou:', erro.code);
  console.log('total do pedido:', pedido.total);
});

carregarPedido('/private/tmp/loja/pedido-9999.json', (erro, pedido) => {
  if (erro) return console.log('falhou:', erro.code);
  console.log('total do pedido:', pedido.total);
});
falhou: ENOENT total do pedido: 479.7

A saída inverteu a ordem das chamadas, e isso é informação real: o arquivo que não existe falha mais rápido do que o arquivo que precisa ser lido do disco. Com callback, a ordem de conclusão não é a ordem do código — nunca conte com ela.

O if (erro) return no começo de cada callback não é frescura de estilo. Sem o return, o código continua e tenta ler pedido.total de um undefined.

Callback hell

O problema aparece quando uma etapa depende da anterior. Cada dependência adiciona um nível:

js
function buscarCliente(id, pronto) {
  setTimeout(() => pronto({ id, nome: 'Ana Souza' }), 10);
}
function buscarCarrinho(cliente, pronto) {
  setTimeout(() => pronto({ cliente: cliente.nome, total: 479.7 }), 10);
}
function calcularFrete(carrinho, pronto) {
  setTimeout(() => pronto({ ...carrinho, frete: carrinho.total >= 199 ? 0 : 24.9 }), 10);
}

buscarCliente(31, (cliente) => {
  buscarCarrinho(cliente, (carrinho) => {
    calcularFrete(carrinho, (fechado) => {
      console.log(fechado);
    });
  });
});
{ cliente: 'Ana Souza', total: 479.7, frete: 0 }

Funciona. Com três etapas ainda dá para ler. Com seis, mais tratamento de erro em cada nível, vira a pirâmide que ficou conhecida como callback hell — e o motivo real não é a indentação, é que não existe um lugar único para tratar falha.

O mesmo fluxo com Promise e await fica plano:

js
const esperar = (ms, valor) => new Promise((resolver) => setTimeout(() => resolver(valor), ms));

const buscarCliente = (id) => esperar(10, { id, nome: 'Ana Souza' });
const buscarCarrinho = (cliente) => esperar(10, { cliente: cliente.nome, total: 479.7 });
const calcularFrete = (c) => esperar(10, { ...c, frete: c.total >= 199 ? 0 : 24.9 });

const cliente = await buscarCliente(31);
const carrinho = await buscarCarrinho(cliente);
console.log(await calcularFrete(carrinho));
{ cliente: 'Ana Souza', total: 479.7, frete: 0 }

Mesmo resultado, sem aninhamento. Repare que o callback não sumiu: ele está dentro do new Promise, na função que recebe resolver. Promise é uma camada por cima de callback, não uma substituta dele.

Erros comuns: os parênteses a mais

Fora do navegador, passar o resultado no lugar da função não dá aviso nenhum — dá exceção, e o rastro aponta para o lugar errado:

js
function freteExpresso(peso) {
  return 24.9 + peso * 3;
}

function orcar(peso, calculadora) {
  return `R$ ${calculadora(peso).toFixed(2)}`;
}

console.log(orcar(2, freteExpresso(2)));
file:///private/tmp/loja/callback-parenteses.mjs:6 return `R$ ${calculadora(peso).toFixed(2)}`; ^

TypeError: calculadora is not a function at orcar (file:///private/tmp/loja/callback-parenteses.mjs:6:16) at file:///private/tmp/loja/callback-parenteses.mjs:9:13 at ModuleJob.run (node:internal/modules/esm/module_job:439:25)

Node.js v24.16.0

O erro cita calculadora, que é o nome do parâmetro — e não freteExpresso, que é onde está o defeito. Quem lê rápido vai investigar orcar, que está correta. A pista de que o problema é na chamada está na segunda linha do rastro: at file:...:9:13 é a linha 9, a do console.log.

Quando você vir X is not a function e X for um parâmetro, olhe a chamada, não a função. Quase sempre são parênteses sobrando.

Do callback ao próximo passo

Três hábitos que evitam a maior parte dos problemas com callback:

  • Nomeie o callback quando ele passar de duas linhas. Uma arrow anônima de quinze linhas dentro de um addEventListener é impossível de testar e some do rastro de erro.
  • Trate o erro no próprio callback e saia com return. Nunca deixe o fluxo continuar depois de detectar falha.
  • Não misture callback e Promise na mesma função. Ou ela recebe um callback, ou ela devolve uma Promise. As duas coisas ao mesmo tempo produzem o bug de chamar o callback duas vezes.

Quando o callback captura variáveis do escopo onde foi escrito — e ele quase sempre captura — o que está em jogo é closure, a lição que explica por que a função lembra de valores que já deveriam ter sumido. E quando o callback é um método de objeto, entra o problema de this em JavaScript. O guia completo de JavaScript mostra em que ponto da trilha cada um desses assuntos entra.

  • callback
  • foreach
  • settimeout
  • addeventlistener
  • assincrono

Perguntas frequentes

Por que o callback vai sem parênteses?
Porque os parênteses executam a função e entregam o resultado dela. Sem parênteses você entrega a função em si, e quem recebe decide quando chamar. Passar com parênteses é como dar o bolo pronto quando pediram a receita.
Todo callback é assíncrono?
Não. O callback de forEach, map e filter roda na hora, dentro da mesma linha. O de setTimeout, de leitura de arquivo e de evento roda depois. A palavra callback só descreve quem chama a função — não quando.
Quando o callback vira problema?
Quando um depende do resultado do anterior. Três etapas encadeadas viram três níveis de indentação, e o tratamento de erro se repete em cada um. Esse é o motivo de existirem Promise e async/await — não é questão de moda.
Posso passar um método de objeto como callback?
Pode, mas ele perde o this no caminho e quebra na primeira propriedade que tentar ler. Passe uma arrow que chame o método, ou use bind para prender o contexto antes.

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 — Callback function — developer.mozilla.org
  2. MDN — EventTarget.addEventListener — developer.mozilla.org
  3. Node.js — Asynchronous flow control — nodejs.org

Continue por aqui