Módulo 3 — JavaScript Assíncrono e APIs
Introdução
No artigo Fetch API: consumindo dados da internet aprendemos a Fetch API — moderna, elegante e baseada em Promises. Mas ela não surgiu do nada. Por mais de uma década, toda comunicação assíncrona entre o navegador e um servidor era feita com o XMLHttpRequest (XHR).
Entender essa história tem valor prático: você vai encontrar código legado com XHR em projetos reais. E mais importante — entender o problema que cada ferramenta resolve faz você apreciar e usar melhor o que temos hoje.
O começo: páginas que recarregavam tudo
Antes de 1999, toda interação com o servidor exigia recarregar a página inteira. Você clicava em "Ver mais comentários" e a página inteira era recarregada — perdendo scroll, estado, e causando aquela piscada incômoda.
O problema era simples: o HTTP é um protocolo de requisição-resposta. O navegador pedia, o servidor respondia com uma página HTML completa, e o navegador a renderizava do zero.
XMLHttpRequest — a revolução silenciosa (1999–2015)
Em 1999, a Microsoft introduziu o XMLHTTP no Internet Explorer 5 para o Outlook Web Access. Em 2005, Jesse James Garrett cunhou o termo AJAX — Asynchronous JavaScript and XML — descrevendo a técnica de atualizar partes de uma página sem recarregar tudo. Em 2006 o W3C publicou o primeiro rascunho de uma especificação para o XMLHttpRequest — que na prática já era universal havia anos, implementado por imitação entre navegadores antes de existir norma alguma.
O Gmail (2004) e o Google Maps (2005) mostraram ao mundo o que era possível. A web nunca mais foi a mesma.
Veja como era uma requisição com XHR:
// Jeito clássico — XMLHttpRequest puro
var xhr = new XMLHttpRequest();
// Configura a requisição
xhr.open("GET", "https://jsonplaceholder.typicode.com/users/1", true);
// true = assíncrono
// Define o que fazer quando a resposta chegar
xhr.onreadystatechange = function() {
// readyState 4 = requisição completa
if (xhr.readyState === 4) {
if (xhr.status === 200) {
var usuario = JSON.parse(xhr.responseText);
console.log("Nome:", usuario.name);
} else {
console.error("Erro:", xhr.status);
}
}
};
// Define handlers de erro
xhr.onerror = function() {
console.error("Falha de rede.");
};
// Envia a requisição
xhr.send();
Funciona. Mas note o quanto é verboso e callback-driven.
Os estados do XMLHttpRequest
O XHR tinha cinco estados de progresso — e você precisava verificar manualmente:
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
switch (xhr.readyState) {
case 0: console.log("UNSENT — objeto criado, open() não chamado"); break;
case 1: console.log("OPENED — open() foi chamado"); break;
case 2: console.log("HEADERS_RECEIVED — headers recebidos"); break;
case 3: console.log("LOADING — corpo chegando"); break;
case 4: console.log("DONE — tudo concluído"); break;
}
};
xhr.open("GET", "https://jsonplaceholder.typicode.com/posts/1");
xhr.send();
Na prática, você só se importava com o estado 4. Mas a API te expunha tudo isso de qualquer forma.
POST com XMLHttpRequest
Enviar dados para o servidor era ainda mais trabalhoso:
var xhr = new XMLHttpRequest();
xhr.open("POST", "https://jsonplaceholder.typicode.com/posts", true);
// Precisa setar o header manualmente
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 201) {
var resposta = JSON.parse(xhr.responseText);
console.log("Post criado com id:", resposta.id);
}
};
// Precisa serializar manualmente
xhr.send(JSON.stringify({
title: "Meu post",
body: "Conteúdo do post",
userId: 1
}));
jQuery.ajax() — a salvação dos anos 2000
O jQuery chegou em 2006 e abstraiu toda essa verbosidade. Por anos, foi a forma dominante de fazer requisições:
// jQuery — muito mais simples que o XHR puro
$.ajax({
url: "https://jsonplaceholder.typicode.com/users/1",
method: "GET",
dataType: "json",
success: function(usuario) {
console.log("Nome:", usuario.name);
},
error: function(xhr, status, erro) {
console.error("Erro:", erro);
}
});
// Versão mais curta
$.getJSON("https://jsonplaceholder.typicode.com/users/1", function(usuario) {
console.log(usuario.name);
});
// POST com jQuery
$.post("https://jsonplaceholder.typicode.com/posts",
{ title: "Meu post", userId: 1 },
function(resposta) {
console.log("Criado:", resposta.id);
}
);
O jQuery resolveu a verbosidade mas ainda era baseado em callbacks — e quando você precisava encadear múltiplas requisições, o Callback Hell voltava.
O Callback Hell do AJAX
Qualquer fluxo com múltiplas requisições dependentes virava isso:
// Autenticação → perfil → permissões → dashboard
// Tudo com jQuery AJAX aninhado
$.ajax({
url: "/api/login",
method: "POST",
data: { email: email, senha: senha },
success: function(auth) {
$.ajax({
url: "/api/perfil/" + auth.userId,
headers: { "Authorization": "Bearer " + auth.token },
success: function(perfil) {
$.ajax({
url: "/api/permissoes/" + perfil.cargo,
success: function(permissoes) {
$.ajax({
url: "/api/dashboard",
success: function(dashboard) {
// finalmente chegamos aqui
renderizarApp(perfil, permissoes, dashboard);
},
error: function() { mostrarErro("Erro ao carregar dashboard"); }
});
},
error: function() { mostrarErro("Erro ao carregar permissões"); }
});
},
error: function() { mostrarErro("Erro ao carregar perfil"); }
});
},
error: function() { mostrarErro("Login inválido"); }
});
Insuportável. E esse era o padrão real de aplicações complexas nos anos 2000 e início dos 2010.
jQuery Deferred — a proto-Promise (2010)
O jQuery tentou resolver isso com os objetos Deferred, que eram uma espécie de Promise antes das Promises existirem:
function buscarUsuario(id) {
return $.getJSON("/api/usuario/" + id); // retorna um Deferred
}
function buscarPedidos(usuarioId) {
return $.getJSON("/api/pedidos/" + usuarioId);
}
// Encadeamento — muito melhor que callbacks aninhados
buscarUsuario(1)
.then(function(usuario) {
console.log("Usuário:", usuario.nome);
return buscarPedidos(usuario.id);
})
.then(function(pedidos) {
console.log("Pedidos:", pedidos.length);
})
.fail(function(erro) {
console.error("Erro:", erro);
});
// Em paralelo
$.when(buscarUsuario(1), buscarPedidos(1))
.done(function(usuario, pedidos) {
console.log(usuario[0].nome, pedidos[0].length);
});
Era melhor — mas a API do jQuery Deferred tinha inconsistências e não era compatível com as Promises nativas que vieram depois.
A linha do tempo completa
1999 — Microsoft cria XMLHTTP no IE5
2004 — Gmail usa AJAX extensivamente pela primeira vez
2005 — Google Maps. Jesse James Garrett cunha o termo "AJAX"
2006 — jQuery 1.0 — $.ajax() simplifica tudo
2006 — W3C publica o primeiro rascunho do XMLHttpRequest
2010 — jQuery Deferred — proto-Promises
2012 — Promises/A+ — especificação da comunidade
2015 — ES6 traz a Promise nativa para a LINGUAGEM
2015 — Fetch é publicado — não pelo ECMAScript, e sim pelo WHATWG:
é API do navegador, não da linguagem (só chegou ao Node em 2022)
2017 — ES2017: async/await
2022 — XMLHttpRequest ainda existe, mas é considerado legado
A comparação direta — mesma operação, três eras
Buscar um usuário e seus posts, tratar erros:
// ── ERA 1: XMLHttpRequest puro (1999–2006) ──────────
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://jsonplaceholder.typicode.com/users/1", true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
var usuario = JSON.parse(xhr.responseText);
var xhr2 = new XMLHttpRequest();
xhr2.open("GET",
"https://jsonplaceholder.typicode.com/posts?userId=" + usuario.id,
true
);
xhr2.onreadystatechange = function() {
if (xhr2.readyState === 4) {
if (xhr2.status === 200) {
var posts = JSON.parse(xhr2.responseText);
console.log(usuario.name + " tem " + posts.length + " posts");
} else {
console.error("Erro ao buscar posts:", xhr2.status);
}
}
};
xhr2.send();
} else {
console.error("Erro ao buscar usuário:", xhr.status);
}
}
};
xhr.send();
// ── ERA 2: jQuery.ajax (2006–2015) ──────────────────
$.getJSON("https://jsonplaceholder.typicode.com/users/1")
.then(function(usuario) {
return $.getJSON(
"https://jsonplaceholder.typicode.com/posts?userId=" + usuario.id
).then(function(posts) {
return { usuario: usuario, posts: posts };
});
})
.done(function(dados) {
console.log(dados.usuario.name + " tem " + dados.posts.length + " posts");
})
.fail(function(erro) {
console.error("Erro:", erro.statusText);
});
// ── ERA 3: Fetch + async/await (2017–hoje) ──────────
async function carregarDados() {
try {
const resUsuario = await fetch(
"https://jsonplaceholder.typicode.com/users/1"
);
if (!resUsuario.ok) throw new Error(`Erro ${resUsuario.status}`);
const usuario = await resUsuario.json();
const resPosts = await fetch(
`https://jsonplaceholder.typicode.com/posts?userId=${usuario.id}`
);
if (!resPosts.ok) throw new Error(`Erro ${resPosts.status}`);
const posts = await resPosts.json();
console.log(`${usuario.name} tem ${posts.length} posts`);
} catch (erro) {
console.error("Erro:", erro.message);
}
}
carregarDados();
A progressão é clara: de 30 linhas aninhadas e confusas para 15 linhas lineares e legíveis.
Quando você ainda vai ver XHR
O XHR ainda tem um caso de uso relevante hoje: monitorar o progresso de upload de arquivos — algo que o Fetch ainda não suporta nativamente:
function uploadComProgresso(arquivo, url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append("arquivo", arquivo);
// Este evento SÓ existe no XHR — Fetch não tem equivalente ainda
xhr.upload.addEventListener("progress", (evento) => {
if (evento.lengthComputable) {
const porcentagem = Math.round((evento.loaded / evento.total) * 100);
console.log(`Upload: ${porcentagem}%`);
atualizarBarraProgresso(porcentagem);
}
});
xhr.addEventListener("load", () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`Erro ${xhr.status}`));
}
});
xhr.addEventListener("error", () => reject(new Error("Falha de rede")));
xhr.addEventListener("abort", () => reject(new Error("Upload cancelado")));
xhr.open("POST", url);
xhr.send(formData);
});
}
// Usando
const input = document.querySelector("#arquivo");
input.addEventListener("change", async () => {
try {
const resultado = await uploadComProgresso(input.files[0], "/api/upload");
console.log("Upload concluído:", resultado);
} catch (erro) {
console.error(erro.message);
}
});
Note que mesmo usando XHR, nós o envolvemos em uma Promise — aproveitando o melhor dos dois mundos.
O futuro: Fetch com progresso
A API de Streams já permite monitorar o progresso de download com Fetch, e o progresso de upload está em desenvolvimento na especificação:
// Progresso de DOWNLOAD com Fetch (já possível hoje)
async function downloadComProgresso(url) {
const response = await fetch(url);
const tamanhoTotal = Number(response.headers.get("Content-Length"));
let recebido = 0;
const reader = response.body.getReader();
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
recebido += value.length;
const porcentagem = Math.round((recebido / tamanhoTotal) * 100);
console.log(`Download: ${porcentagem}%`);
}
// Monta o resultado final
const totalBytes = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
const resultado = new Uint8Array(totalBytes);
let offset = 0;
for (const chunk of chunks) {
resultado.set(chunk, offset);
offset += chunk.length;
}
return resultado;
}
Resumo da evolução
| Característica | XHR Puro | jQuery.ajax | Fetch + async/await |
|---|---|---|---|
| Verbosidade | Alta | Média | Baixa |
| Baseado em | Callbacks | Callbacks / Deferred | Promises nativas |
| Tratamento de erro | Manual e verboso | .fail() | try/catch |
| Legibilidade | Baixa | Média | Alta |
| Sem dependência | ✅ | ❌ (jQuery) | ✅ |
| Cancelamento | xhr.abort() | xhr.abort() | AbortController |
| Progresso de upload | ✅ | ✅ | ⚠️ ainda limitado |
| Streams | ❌ | ❌ | ✅ |
| Status em erros HTTP | Manual | Manual | Manual (response.ok) |
Quinze anos separam o XMLHttpRequest do fetch, e o que mudou nesse intervalo não foi a capacidade — requisição assíncrona o XHR sempre fez — e sim o vocabulário disponível para expressá-la. Os cinco estados conferidos à mão viraram uma Promise, e o $.ajax do jQuery foi o intermediário que experimentou a forma que a plataforma acabaria adotando. O XHR, aliás, não morreu: ainda é o único caminho para acompanhar o progresso de um upload.
Fontes e Referências
- MDN Web Docs — XMLHttpRequest: https://developer.mozilla.org/pt-BR/docs/Web/API/XMLHttpRequest
- MDN Web Docs — XMLHttpRequest.upload: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/upload
- MDN Web Docs — Streams API: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API
- jQuery Docs — $.ajax: https://api.jquery.com/jquery.ajax
- Ajax — histórico e definição: https://en.wikipedia.org/wiki/Ajax_(programming)
- JavaScript.info — XMLHttpRequest: https://javascript.info/xmlhttprequest
- JavaScript: The Good Parts — Douglas Crockford (O'Reilly)
- A história do jQuery: https://jquery.com/
Exercícios
Exercício 1
Este XHR verifica readyState === 4 mas não olha o status. O que o usuário vê quando o servidor responde 404 com uma página de erro em HTML?
var xhr = new XMLHttpRequest();
xhr.open("GET", "/api/usuario/999", true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
var usuario = JSON.parse(xhr.responseText);
document.querySelector("#nome").textContent = usuario.nome;
}
};
xhr.send();
Ver resposta
✓ Resposta: O JSON.parse estoura com SyntaxError: Unexpected token '<', o handler morre no meio e o nome nunca é preenchido — a tela fica parada, sem mensagem nenhuma. O readyState === 4 significa apenas que a resposta terminou de chegar, não que ela foi bem-sucedida: um 404, um 500 e um 200 chegam todos ao estado 4. É por isso que o padrão correto da época era testar as duas coisas, xhr.readyState === 4 && xhr.status === 200. Repare que esse é exatamente o mesmo descuido que reaparece hoje com o fetch, na forma de esquecer o response.ok — a ferramenta mudou, a armadilha continua a mesma, porque em ambas o erro HTTP é uma resposta e não uma falha. E note o detalhe do sintoma: o < da mensagem é o primeiro caractere do <!DOCTYPE html> da página de erro, uma pista que aponta para o parser quando a causa está na URL.
Exercício 2
Nas duas eras, o que acontece com o segundo pedido quando o primeiro falha?
// ERA 1 — XHR aninhado
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
var usuario = JSON.parse(xhr.responseText);
// ... aqui dentro é criado o xhr2
} else {
console.error("Erro ao buscar usuário");
}
};
// ERA 3 — async/await
const usuario = await (await fetch(url1)).json();
const posts = await (await fetch(url2 + usuario.id)).json();
Ver resposta
✓ Resposta: Na ERA 1 o segundo pedido simplesmente não acontece, porque ele só existe dentro do ramo de sucesso — e essa é a parte boa. A parte ruim é o que o tratamento de erro custava: cada nível precisava do próprio else, com a própria mensagem, e um nível esquecido virava falha silenciosa. Na ERA 3 o segundo await nunca é alcançado, pelo mesmo motivo pelo qual uma linha depois de um throw não executa: a rejeição interrompe a função ali. A diferença é que o tratamento acontece em um lugar só, no try/catch que envolve tudo, em vez de espalhado por cada nível. Vale reparar num defeito que o trecho da ERA 3 tem: (await fetch(url)).json() pula a checagem de response.ok, e por isso comete exatamente o erro do exercício anterior — a sintaxe ficou moderna, o descuido permaneceu.
Exercício 3
O servidor envia a resposta com Transfer-Encoding: chunked e portanto sem Content-Length. O que a barra de progresso mostra?
const response = await fetch(url);
const tamanhoTotal = Number(response.headers.get("Content-Length"));
let recebido = 0;
// ... dentro do laço de leitura:
recebido += value.length;
const porcentagem = Math.round((recebido / tamanhoTotal) * 100);
console.log(`Download: ${porcentagem}%`);
Ver resposta
✓ Resposta: Mostra Download: Infinity% a cada pedaço recebido. O headers.get devolve null quando o cabeçalho não existe, e Number(null) é 0 — não NaN, o que seria menos traiçoeiro, porque NaN ao menos apareceria como "NaN%" e denunciaria o problema. Dividir por zero em JavaScript não lança erro: produz Infinity, que atravessa o Math.round intacto. E a ausência de Content-Length não é caso raro: acontece sempre que a resposta é gerada em fluxo, e também quando há compressão, situação em que o cabeçalho pode informar o tamanho comprimido enquanto o leitor entrega bytes já descomprimidos — aí a barra passa de 100%. O tratamento correto é verificar antes se o total é conhecido e maior que zero, e, quando não for, mostrar um indicador indeterminado em vez de uma porcentagem inventada.
Exercício 4
Por que este upload precisa de XMLHttpRequest, se o fetch é a API moderna? E por que ele é embrulhado numa Promise?
xhr.upload.addEventListener("progress", (evento) => {
if (evento.lengthComputable) {
atualizarBarra(Math.round((evento.loaded / evento.total) * 100));
}
});
Ver resposta
✓ Resposta: Porque o fetch não expõe o progresso de envio. Ele consegue relatar o progresso de download, lendo response.body como fluxo, mas o corpo que sai do navegador não tem um evento equivalente amplamente disponível — o XHR tem, no objeto xhr.upload, e é o único motivo pelo qual essa API de 1999 continua em uso em código novo. O lengthComputable aparece porque o tamanho nem sempre é conhecido de antemão, o mesmo problema do exercício anterior visto do outro lado. Quanto ao embrulho: XHR é baseado em eventos, e eventos não se encaixam em await nem em try/catch. Envolver o objeto num new Promise — resolvendo no load, rejeitando no error e no abort — traduz a API antiga para o vocabulário atual, e é o padrão geral para adaptar qualquer API de callback ao mundo das Promises. O nome disso é promisify, e o Node traz até um utilitário pronto, o util.promisify.
Exercício 5
A linha do tempo do artigo separa duas coisas que costumam ser confundidas. A Promise e o fetch vieram do mesmo lugar? O que isso explica na prática?
// Node.js 16, em 2021:
Promise.resolve(1).then(v => console.log(v)); // funciona
fetch("https://exemplo.com"); // ReferenceError: fetch is not defined
Ver resposta
✓ Resposta: Não vieram. A Promise faz parte da linguagem, definida pelo ECMAScript e presente em qualquer lugar onde haja um motor JavaScript — navegador, Node, Deno, um videogame. O fetch é uma API de ambiente, especificada pelo WHATWG junto com o resto da plataforma web, e existe apenas onde alguém decidiu implementá-la. Daí o erro do exemplo: o Node só passou a trazer fetch embutido na versão 18, em 2022, e é por isso que projetos anteriores dependiam de pacotes como node-fetch ou axios. A mesma divisão explica outras estranhezas do dia a dia: setTimeout, localStorage, document e console também não pertencem à linguagem, o que é a razão de document não existir no Node e de require não existir no navegador. Saber de qual lado da fronteira está cada coisa poupa muito tempo diante de um ReferenceError.