Javascript

Callbacks: o começo de tudo Já leu

10 min de leitura

Callbacks: o começo de tudo
No artigo anterior entendemos o Event Loop — o mecanismo que permite ao JavaScript ser assíncrono mesmo sendo single-threaded. Agora vamos entender

No artigo anterior entendemos o Event Loop — o mecanismo que permite ao JavaScript ser assíncrono mesmo sendo single-threaded. Agora vamos entender o primeiro padrão que surgiu para lidar com essa assincronicidade: os callbacks.

Callbacks não são exclusivos do código assíncrono — você já os usou bastante nos módulos anteriores. Mas é no contexto assíncrono que eles revelam tanto seu poder quanto seus problemas.


O que é um callback?

Um callback é simplesmente uma função passada como argumento para outra função, que será executada em algum momento — imediatamente ou depois.

// Callback síncrono — executado imediatamente
const numeros = [3, 1, 4, 1, 5, 9];

numeros.forEach(function(numero) {
  console.log(numero); // esta função é um callback
});

// Com arrow function — mais comum
numeros.forEach(numero => console.log(numero));

// filter também usa callback
const pares = numeros.filter(n => n % 2 === 0);

Você já usou callbacks o tempo todo — forEach, map, filter, reduce, addEventListener — todos recebem callbacks. A diferença agora é que vamos usar callbacks para operações assíncronas.


Callbacks assíncronos

Um callback assíncrono é executado depois — quando uma operação demorada termina:

console.log("Antes");

// O callback só executa após 2 segundos
setTimeout(function() {
  console.log("Dentro do callback");
}, 2000);

console.log("Depois");

// Saída:
// Antes
// Depois
// (2 segundos depois)
// Dentro do callback

O JavaScript não espera — ele registra o callback e continua. Quando o tempo passa, o Event Loop coloca o callback na fila para executar.


Simulando operações assíncronas

Em exemplos didáticos, usamos setTimeout para simular operações que levam tempo — como buscar dados de um servidor:

function buscarUsuario(id, callback) {
  console.log(`Buscando usuário ${id}...`);

  // Simula 1.5 segundos de espera (como uma requisição real)
  setTimeout(function() {
    const usuario = {
      id,
      nome: "Ana Paula",
      email: "ana@email.com",
      plano: "premium",
    };
    callback(usuario); // chama o callback com o resultado
  }, 1500);
}

// Usando a função — passamos o que fazer com o resultado
buscarUsuario(42, function(usuario) {
  console.log(`Usuário encontrado: ${usuario.nome}`);
  console.log(`Plano: ${usuario.plano}`);
});

console.log("Código continua executando...");

// Saída:
// Buscando usuário 42...
// Código continua executando...
// (1.5 segundos depois)
// Usuário encontrado: Ana Paula
// Plano: premium

O padrão error-first callback

O Node.js popularizou uma convenção importante para callbacks assíncronos: o primeiro parâmetro é sempre o erro, e o segundo é o resultado. Isso garante consistência no tratamento de falhas:

function buscarProduto(id, callback) {
  setTimeout(function() {
    if (id <= 0) {
      // Primeiro argumento: o erro
      callback(new Error("ID inválido — deve ser maior que zero."), null);
      return;
    }

    if (id > 100) {
      callback(new Error(`Produto ${id} não encontrado.`), null);
      return;
    }

    // Segundo argumento: o resultado
    callback(null, {
      id,
      nome: "Notebook Pro",
      preco: 3500,
      estoque: 15,
    });
  }, 1000);
}

// Usando — sempre verifique o erro primeiro
buscarProduto(42, function(erro, produto) {
  if (erro) {
    console.error(`Erro: ${erro.message}`);
    return; // para aqui se houver erro
  }

  console.log(`Produto: ${produto.nome}`);
  console.log(`Preço: R$ ${produto.preco}`);
});

buscarProduto(-5, function(erro, produto) {
  if (erro) {
    console.error(`Erro: ${erro.message}`); // Erro: ID inválido
    return;
  }
  console.log(produto.nome);
});

O padrão é sempre: callback(erro, resultado). Se deu certo, erro é null. Se deu errado, resultado é null.


Callbacks aninhados — operações dependentes

O problema real começa quando uma operação depende do resultado de outra:

function buscarUsuario(id, cb) {
  setTimeout(() => {
    cb(null, { id, nome: "Carlos", enderecoId: 7 });
  }, 800);
}

function buscarEndereco(enderecoId, cb) {
  setTimeout(() => {
    cb(null, { id: enderecoId, rua: "Av. Brasil", cidade: "São Paulo" });
  }, 600);
}

function buscarPedidos(usuarioId, cb) {
  setTimeout(() => {
    cb(null, [
      { id: 101, total: 250 },
      { id: 102, total: 180 },
    ]);
  }, 700);
}

function calcularFrete(cidade, cb) {
  setTimeout(() => {
    cb(null, { cidade, valor: 25.90, prazo: "3 dias úteis" });
  }, 500);
}

// Para fazer tudo isso em sequência — chegamos ao Callback Hell
buscarUsuario(1, function(erro, usuario) {
  if (erro) return console.error(erro);

  buscarEndereco(usuario.enderecoId, function(erro, endereco) {
    if (erro) return console.error(erro);

    buscarPedidos(usuario.id, function(erro, pedidos) {
      if (erro) return console.error(erro);

      calcularFrete(endereco.cidade, function(erro, frete) {
        if (erro) return console.error(erro);

        // Finalmente chegamos ao resultado
        console.log(`Usuário: ${usuario.nome}`);
        console.log(`Cidade: ${endereco.cidade}`);
        console.log(`Pedidos: ${pedidos.length}`);
        console.log(`Frete: R$ ${frete.valor} — ${frete.prazo}`);

        // E se precisarmos de mais um passo? Mais um nível...
      });
    });
  });
});

Isso é o Callback Hell — também chamado de "pyramid of doom" pela forma que o código toma. Os problemas são claros:

  • Difícil de ler — cresce para a direita indefinidamente
  • Difícil de manter — alterar a ordem é trabalhoso
  • Difícil de tratar erros — cada nível precisa verificar o erro
  • Impossível de reusar — lógica toda acoplada

Amenizando o Callback Hell — funções nomeadas

Uma solução parcial é extrair os callbacks em funções nomeadas:

// Em vez de aninhar tudo, quebramos em funções nomeadas

function aoReceberUsuario(erro, usuario) {
  if (erro) return tratarErro(erro);
  buscarEndereco(usuario.enderecoId, aoReceberEndereco.bind(null, usuario));
}

function aoReceberEndereco(usuario, erro, endereco) {
  if (erro) return tratarErro(erro);
  buscarPedidos(usuario.id, aoReceberPedidos.bind(null, usuario, endereco));
}

function aoReceberPedidos(usuario, endereco, erro, pedidos) {
  if (erro) return tratarErro(erro);
  calcularFrete(endereco.cidade, aoReceberFrete.bind(null, usuario, endereco, pedidos));
}

function aoReceberFrete(usuario, endereco, pedidos, erro, frete) {
  if (erro) return tratarErro(erro);
  console.log(`Usuário: ${usuario.nome}`);
  console.log(`Cidade: ${endereco.cidade}`);
  console.log(`Pedidos: ${pedidos.length}`);
  console.log(`Frete: R$ ${frete.valor}`);
}

function tratarErro(erro) {
  console.error(`Erro: ${erro.message}`);
}

// Inicia a cadeia
buscarUsuario(1, aoReceberUsuario);

Melhor — pelo menos o código não cresce para a direita. Mas ainda é complicado gerenciar o estado entre os níveis e o fluxo não é linear e legível.


Callbacks em paralelo

Às vezes as operações não dependem umas das outras — podem executar simultaneamente. O desafio é saber quando todas terminaram:

function buscarDadosParalelo(ids, callback) {
  const resultados = [];
  let concluidos = 0;
  let houveErro = false;

  ids.forEach((id, index) => {
    buscarProduto(id, function(erro, produto) {
      if (houveErro) return; // já deu errado em outro

      if (erro) {
        houveErro = true;
        return callback(erro, null);
      }

      resultados[index] = produto; // mantém a ordem
      concluidos++;

      if (concluidos === ids.length) {
        callback(null, resultados); // todos concluíram!
      }
    });
  });
}

buscarDadosParalelo([1, 5, 12, 30], function(erro, produtos) {
  if (erro) return console.error(erro);
  console.log(`${produtos.length} produtos carregados.`);
  produtos.forEach(p => console.log(`- ${p.nome}: R$ ${p.preco}`));
});

Isso funciona, mas é verboso e propenso a bugs. As Promises resolvem isso com muito mais elegância — como veremos no próximo artigo.


Casos onde callbacks ainda são a escolha certa

Apesar dos problemas, callbacks são a solução ideal em vários cenários:

// 1. Eventos do DOM — chamados várias vezes
botao.addEventListener("click", (e) => {
  console.log("Clicado!");
});

// 2. Métodos de array — síncronos e funcionais
const dobrados = [1, 2, 3].map(n => n * 2);
const pares = [1, 2, 3, 4].filter(n => n % 2 === 0);

// 3. Operações simples com setTimeout
setTimeout(() => limparMensagem(), 3000);

// 4. APIs que retornam múltiplos eventos ao longo do tempo
// (Streams, WebSockets — Promises só resolvem uma vez)
stream.on("data", (chunk) => processar(chunk));
stream.on("end", () => finalizar());
stream.on("error", (err) => tratarErro(err));

Para operações únicas que levam tempo e podem falhar, as Promises são superiores. Para eventos recorrentes, callbacks são a solução natural.


Exemplo completo — sistema de notificações com callbacks

// Sistema que demonstra callbacks de forma organizada

const sistemaBD = {
  usuarios: [
    { id: 1, nome: "Ana", email: "ana@email.com", notificacoesAtivas: true },
    { id: 2, nome: "Bruno", email: "bruno@email.com", notificacoesAtivas: false },
    { id: 3, nome: "Clara", email: "clara@email.com", notificacoesAtivas: true },
  ],

  buscarUsuario(id, cb) {
    setTimeout(() => {
      const usuario = this.usuarios.find(u => u.id === id);
      if (!usuario) return cb(new Error(`Usuário ${id} não encontrado.`));
      cb(null, usuario);
    }, 500);
  },

  buscarTodos(cb) {
    setTimeout(() => {
      cb(null, [...this.usuarios]);
    }, 600);
  },
};

const emailService = {
  enviar(destinatario, mensagem, cb) {
    setTimeout(() => {
      if (!destinatario.includes("@")) {
        return cb(new Error(`E-mail inválido: ${destinatario}`));
      }
      console.log(`📧 E-mail enviado para ${destinatario}: "${mensagem}"`);
      cb(null, { enviado: true, destinatario });
    }, 300);
  },
};

// Notificar um único usuário
function notificarUsuario(id, mensagem, cb) {
  sistemaBD.buscarUsuario(id, function(erro, usuario) {
    if (erro) return cb(erro);

    if (!usuario.notificacoesAtivas) {
      return cb(null, { enviado: false, motivo: "Notificações desativadas." });
    }

    emailService.enviar(usuario.email, mensagem, function(erro, resultado) {
      if (erro) return cb(erro);
      cb(null, { ...resultado, usuario: usuario.nome });
    });
  });
}

// Notificar todos os usuários com notificações ativas
function notificarTodos(mensagem, cb) {
  sistemaBD.buscarTodos(function(erro, usuarios) {
    if (erro) return cb(erro);

    const ativos = usuarios.filter(u => u.notificacoesAtivas);
    const resultados = [];
    let concluidos = 0;

    if (ativos.length === 0) return cb(null, []);

    ativos.forEach(usuario => {
      emailService.enviar(usuario.email, mensagem, function(erro, resultado) {
        concluidos++;

        if (erro) {
          resultados.push({ usuario: usuario.nome, erro: erro.message });
        } else {
          resultados.push({ usuario: usuario.nome, ...resultado });
        }

        if (concluidos === ativos.length) {
          cb(null, resultados);
        }
      });
    });
  });
}

// Testando
console.log("Iniciando sistema de notificações...\n");

notificarUsuario(1, "Seu pedido foi aprovado!", function(erro, resultado) {
  if (erro) return console.error(`Erro: ${erro.message}`);
  console.log(`Resultado para usuário 1:`, resultado);
});

notificarUsuario(2, "Promoção especial para você!", function(erro, resultado) {
  if (erro) return console.error(`Erro: ${erro.message}`);
  console.log(`Resultado para usuário 2:`, resultado);
});

notificarTodos("Manutenção programada para domingo.", function(erro, resultados) {
  if (erro) return console.error(`Erro geral: ${erro.message}`);
  console.log("\nResultados do envio em massa:");
  resultados.forEach(r => console.log(` - ${r.usuario}: ${r.enviado ? "✓" : "✗"}`));
});

Boas práticas com callbacks

// ✅ 1. Sempre siga o padrão error-first
function operacao(params, callback) {
  // callback(erro, resultado)
  callback(null, resultado); // sucesso
  callback(new Error("msg"), null); // falha
}

// ✅ 2. Sempre verifique o erro primeiro
operacao(params, function(erro, resultado) {
  if (erro) {
    console.error(erro);
    return; // pare aqui
  }
  // use resultado
});

// ✅ 3. Nunca chame o callback duas vezes
function operacaoSegura(cb) {
  let chamou = false;
  setTimeout(() => {
    if (chamou) return;
    chamou = true;
    cb(null, "resultado");
  }, 1000);
}

// ✅ 4. Extraia callbacks em funções nomeadas
//    para evitar o Callback Hell
function aoReceberDados(erro, dados) { /* ... */ }
operacao(params, aoReceberDados);

// ✅ 5. Para múltiplas operações assíncronas,
//    prefira Promises ou async/await (próximos artigos)

Tarefa para você

Implemente um sistema de carrinho de compras assíncrono usando apenas callbacks:

// Funções disponíveis (implemente com setTimeout):
// buscarProduto(id, callback)  → retorna produto ou erro
// verificarEstoque(id, quantidade, callback) → retorna true/false ou erro
// aplicarCupom(codigo, subtotal, callback) → retorna valor com desconto ou erro
// finalizarPedido(itens, total, callback) → retorna pedido ou erro

// O fluxo deve ser:
// 1. Buscar 2 produtos (em paralelo)
// 2. Verificar estoque de ambos (em paralelo)
// 3. Calcular subtotal
// 4. Aplicar cupom (se fornecido)
// 5. Finalizar pedido
// 6. Exibir resumo ou erro em cada etapa

// Dica: use o padrão de paralelo que vimos para os passos 1 e 2

Conclusão

Neste artigo você aprendeu:

  • O que é um callback e como já os usávamos nos módulos anteriores
  • A diferença entre callbacks síncronos e assíncronos
  • O padrão error-first callback do Node.js
  • Como callbacks se aninham em operações dependentes
  • O Callback Hell e seus problemas reais
  • Como amenizar com funções nomeadas
  • Como executar callbacks em paralelo
  • Quando callbacks ainda são a escolha certa
  • Boas práticas para código com callbacks

 

📚 Fontes e Referências

Comentários

Mais em Javascript

Eventos: click, input, submit e muito mais
Eventos: click, input, submit e muito mais

No artigo anterior aprendemos a selecionar e modificar elementos do DOM. Mas...

Async/Await: escrevendo código assíncrono de forma limpa
Async/Await: escrevendo código assíncrono de forma limpa

As Promises resolveram o Callback Hell. Mas encadear muitos&nbsp;.then() aind...

Dominando o JavaScript
Dominando o JavaScript

Estou estudando Javascript a um longo tempo. N&atilde;o sei precisar quanto t...