Entendendo Promises e a API Fetch no Javascript
As Promises e a API fetch são ferramentas essenciais no dia a dia do desenvolvedor Javascript moderno. As Promises facilitam o tratamento de operações assíncronas, evitando o antigo problema do callback hell que era comum em códigos mais antigos. Por sua vez, a API fetch oferece uma forma moderna e simplificada de realizar requisições HTTP diretamente do navegador — algo especialmente relevante no contexto atual, onde aplicações consomem APIs REST com frequência.
Antes de nos aprofundarmos na API fetch, é importante entender um pouco o funcionamento das Promises, já que essa API se baseia nelas para retornar e manipular os resultados das requisições.
Nem todas as operações em Javascript são executadas de forma imediata. Em muitas situações, lidamos com tarefas que podem levar algum tempo para serem concluídas, como:
- Realizar uma requisição a uma API REST;
- Ler o conteúdo de um arquivo;
- Executar cálculos complexos ou demorados.
Se executarmos estas operações e aguardarmos até que a operação seja concluída para seguir executando o restante do código, isso pode gerar travamentos perceptíveis na interface do usuário, especialmente em aplicações que dependem de interações rápidas e contínuas.
Para entender melhor esse conceito, vamos analisar um exemplo prático em ação.
Exemplo: soma de números primos
A função a seguir realiza a soma de todos os números primos até um determinado limite. O objetivo aqui não é detalhar o cálculo em si, mas destacar que essa operação consome um tempo considerável de processamento.
function calcularTotalPrimos(limite) {
let totalPrimos = 0;
for (let i = 2; i <= limite; i++) {
let ehPrimo = true;
for (let j = 2; j < i; j++) {
if (i % j === 0) {
ehPrimo = false;
break;
}
}
if (ehPrimo) totalPrimos += i;
}
return totalPrimos;
}
Agora, vamos executar essa função para observar como uma operação pesada pode impactar a performance da aplicação.
document.querySelector("#titulo").textContent = "Exemplo de Soma de Primos - Sincrono";
let primo = calcularTotalPrimos(300000);
document.querySelector("#soma-primo").textContent = "Resultado: " + primo;
document.querySelector("#container").textContent = "Outras modificações no DOM";
Ao rodar esse código, você vai perceber que a página congela completamente durante a execução do cálculo. Enquanto a função está sendo processada, nenhuma outra interação acontece — atualizações no DOM, cliques, animações ficam paralisados. Esse é um exemplo clássico de travamento causado por uma operação pesada sendo executada de forma bloqueante na thread principal do Javascript.
Uma solução simples com setTimeout
Uma forma simples de contornar esse bloqueio é usar setTimeout com um atraso de 0 milissegundos. Isso adia a execução da função para o próximo ciclo do event loop, permitindo que o navegador finalize tarefas pendentes — como renderizar a interface ou responder a eventos do usuário — antes de iniciar o processamento pesado.
document.querySelector("#titulo").textContent = "Exemplo de Soma de Primos Assincrona";
setTimeout( () => {
let primo = calcularTotalPrimos(300000);
document.querySelector("#soma-primo").textContent = "Resultado: " + primo;
}, 0);
document.querySelector("#container").textContent = "Outras modificações no DOM";
Agora, ao executarmos o exemplo, o navegador tem a chance de atualizar o conteúdo da página antes de iniciar o cálculo pesado. Isso acontece porque o uso de setTimeout permite que a renderização ocorra antes da função ser processada.
O problema de sempre ter que lembrar do setTimeout
Embora essa abordagem funcione, ela tem uma armadilha: é necessário lembrar de sempre envolver o cálculo com um setTimeout. Caso isso seja esquecido, o código volta a bloquear a interface — e esse comportamento pode passar despercebido em testes rápidos. No exemplo que usamos, o tempo de execução é previsível, pois o argumento passado é sempre o mesmo. No entanto, se estivermos lidando com operações cujo tempo varia, como chamadas de rede, o problema se agrava. Durante os testes, essas chamadas podem parecer rápidas, mas em produção, onde há mais variabilidade, o bloqueio pode se tornar perceptível e prejudicar a experiência do usuário.
Encapsulando a lógica para evitar problemas
Uma alternativa mais robusta é encapsular o setTimeout dentro da própria função calcularTotalPrimos. No entanto, ao adiar a execução, não podemos mais simplesmente retornar o resultado, pois ele só estará disponível depois, dentro da função agendada. Como return não funciona nesse cenário, uma solução é passar uma função de callback como argumento. Essa função será chamada com o resultado do cálculo assim que ele for concluído.
function calcularTotalPrimos(limite, callback) {
setTimeout(() => {
let totalPrimos = 0;
for (let i = 2; i <= limite; i++) {
let ehPrimo = true;
for (let j = 2; j < i; j++) {
if (i % j === 0) {
ehPrimo = false;
break;
}
}
if (ehPrimo) totalPrimos += i;
}
callback(totalPrimos);
}, 0);
}
Agora, a chamada da função precisa passar uma função de callback como argumento. Essa função será executada automaticamente assim que o cálculo for concluído, recebendo o resultado como parâmetro.
document.querySelector("#titulo").textContent = "Exemplo de Soma de Primos";
calcularTotalPrimos(300000, (primo) => {
document.querySelector("#soma-primo").textContent = primo;
});
document.querySelector("#outro-texto").textContent = "Após soma total de primos";
Muitas bibliotecas seguem esse padrão e utilizam funções de callback nomeadas — como onSuccess, onError ou onComplete — para lidar com os diferentes resultados de operações assíncronas. A própria biblioteca é responsável por chamar o callback adequado ao final do processo, seja em caso de sucesso ou falha.
No entanto, essa abordagem tem uma limitação importante: e se, dentro do callback, for necessário chamar outra função assíncrona que também depende de um callback? E se essa, por sua vez, chamar mais uma? Com o tempo, o código se torna difícil de ler, manter e entender. Esse padrão de aninhamento excessivo ficou conhecido como callback hell — ou “inferno dos callbacks”.
calcularTotalPrimos(300000, (primo) => {
outroCalculo(primo, (result) =>{
operacao2( result, (result2) => {
// utiliza o result2
});
})
});
Promises
Para resolver o problema do callback hell e tornar o código assíncrono mais organizado e legível, o Javascript introduziu o conceito de Promises (promessas). Uma Promise é uma classe nativa do Javascript que representa o resultado futuro de uma operação assíncrona — algo que ainda não está disponível, mas que será resolvido em algum momento, seja com sucesso ou falha.
Ao ser criada e retornada, uma Promise atua como um contêiner para esse valor futuro. Ela facilita a construção de funções assíncronas sem depender do aninhamento de callbacks, permitindo o encadeamento de operações com os métodos .then() (para sucesso) e .catch() (para erros). Uma Promise possui três estados:
- Pending (pendente): o processamento ainda está em andamento;
- Fulfilled (resolvida): a operação foi concluída com sucesso;
- Rejected (rejeitada): ocorreu algum erro durante a execução.
Criando uma Promise
Para criar uma Promise, usamos o construtor new Promise(), que recebe como argumento uma função com dois parâmetros: resolve e reject, que são duas funções de callback. Essa função representa o trabalho assíncrono que será executado. Quando a operação for concluída com sucesso, chamamos resolve(valor) para indicar que a Promise foi cumprida. Se ocorrer algum erro, usamos reject(erro) para sinalizar a falha.
Para ilustrar vamos criar um exemplo simples de Promise que simula uma operação assíncrona com duração de 2 segundos e que possui 50% de chance de falhar. Esse tipo de simulação é útil para entender como lidar com sucesso e erro em tarefas assíncronas.
let promise = new Promise( (resolve, reject) => {
setTimeout( () =>{
let success = Math.random() < 0.5;
if(success){
resolve("Funcionou!!!");
}else{
reject(new Error("Deu Erro!!!"));
}
}, 2000);
});
Em seguida, vamos adicionar as funções que tratam o sucesso e a falha da Promise.
promise
.then( (value) =>{
console.log(value)
})
.catch( (error) =>{
console.log(error);
})
.finally( () =>{
console.log("Terminou");
});
Agora vamos explorar os métodos da Promise usados para configurar os callbacks que serão executados após a conclusão da operação assíncrona, seja ela bem-sucedida ou não.
then: define o que deve ser feito quando a promise for resolvida com sucesso. Ele recebe uma função de callback que será chamada com o valor retornado pela operação assíncrona.catch: especifica o que deve acontecer se a promise for rejeitada — ou seja, se ocorrer um erro durante a execução. Ele recebe uma função de callback que trata esse erro.finally: define um bloco de código que será executado independentemente de a promise ter sido resolvida ou rejeitada..
Encadeando Promises
Os métodos de uma Promise — como then, catch e finally — retornam uma nova Promise. Isso permite que múltiplas operações assíncronas sejam encadeadas de forma fluida, com cada etapa aguardando a resolução da anterior. Essa característica torna o código mais organizado, evitando o aninhamento excessivo e facilitando a leitura e manutenção.
function func1(arg){
return new Promise( (resolve, reject) =>{
setTimeout(() =>{
resolve("("+arg+")");
},1000);
})
}
function func2(arg){
return new Promise( (resolve, reject) =>{
setTimeout(() =>{
resolve("["+arg+"]");
},1000);
});
}
func1("BOTECO")
.then( (arg) =>{
return func2(arg);
})
.then( (arg) =>{
console.log(arg);
});
// No console: [(BOTECO)]
No exemplo acima, criamos duas funções que retornam Promises e simulam operações assíncronas com um atraso de 1 segundo cada. A func1 envolve o valor recebido entre parênteses, enquanto a func2 o envolve entre colchetes. Ao chamar func1("BOTECO"), usamos o método .then() para definir a função de sucesso que recebe o resultado de func1 e o passa para func2. Como func2 também retorna uma Promise, podemos encadear outro .then() para lidar com o resultado final de forma sequencial e organizada.
Agora que compreendemos melhor o funcionamento das Promises, podemos reescrever a nossa função calcularTotalPrimos em uma nova versão que, em vez de receber uma função de callback, retorna uma Promise. Isso tornará o código mais organizado e facilitará o encadeamento com outras operações assíncronas.
function calcularTotalPrimos(limite) {
return new Promise( (resolve, reject) => {
setTimeout( () => {
let totalPrimos = 0;
for (let i = 2; i <= limite; i++) {
let ehPrimo = true;
for (let j = 2; j < i ; j++) {
if (i % j === 0) {
ehPrimo = false;
break;
}
}
if (ehPrimo) totalPrimos += i;
}
resolve(totalPrimos);
},0 );
});
}
Com isso, já podemos utilizá-la em nosso código.
document.querySelector("#titulo").innerHTML = "Exemplo de Soma de Primos Assincrona";
calcularTotalPrimos(300000)
.then( (primo) => {
document.querySelector("#soma-primo").innerHTML = primo;
});
document.querySelector("#outro-texto").innerHTML = "Após soma total primos";
É importante destacar que, para um caso como o da função calcularTotalPrimos, a abordagem mais adequada seria utilizar um Web Worker, que permite a execução de código em uma thread separada, sem bloquear a interface principal do navegador. Neste exemplo, usamos o setTimeout apenas como uma forma simplificada de simular comportamento assíncrono, mas ele não resolve o problema de desempenho em cálculos pesados — apenas adia sua execução momentaneamente.
async e await
A sintaxe async/await oferece uma maneira ainda mais legível e intuitiva de trabalhar com Promises, permitindo que operações assíncronas sejam escritas de forma semelhante ao código síncrono tradicional.
A palavra-chave async deve ser usada antes da declaração de uma função para indicar que ela sempre retornará uma Promise. Mesmo que a função não retorne explicitamente uma Promise, o valor retornado será automaticamente encapsulado em uma Promise resolvida. Isso garante que toda função marcada como async tenha um comportamento assíncrono previsível.
async function f(){
return 42;
}
f().then( (value) =>{
console.log(value);
});
O exemplo anterior é funcionalmente equivalente a escrever o retorno da função como uma Promise resolvida de forma explícita:
async function f(){
return new Promise( (resolve, reject) => {
resolve(42);
});
}
f().then( (value) =>{
console.log(value);
});
A instrução await faz com que o Javascript pause a execução da função assíncrona até que a Promise seja resolvida (com sucesso) ou rejeitada (com erro), retornando o valor resolvido ou lançando a exceção correspondente. Isso permite escrever código assíncrono de forma mais clara e sequencial, como se fosse síncrono.
async function f(){
const promise = new Promise( (resolve, reject) => {
setTimeout( () => {
resolve("Tudo Certo!");
}, 1000);
});
let value = await promise;
console.log(value);
}
f();
Na linha 8 do exemplo anterior, a execução da função é temporariamente pausada até que a Promise seja resolvida. O valor retornado por essa espera será exatamente o argumento passado para a função resolve dentro da Promise.
Observação: A palavra-chave await só pode ser usada dentro de funções declaradas com async. Isso garante que o Javascript saiba que está lidando com um contexto assíncrono e que pode pausar a execução até que a Promise aguardada seja resolvida ou rejeitada.
Fetch API
A API Fetch é uma interface moderna do Javascript utilizada para realizar requisições de rede de forma assíncrona. Ela permite que desenvolvedores façam chamadas a APIs web de maneira simples e elegante, retornando uma Promise que facilita o tratamento das respostas e possíveis erros, proporcionando um fluxo de código mais limpo e legível.
Para fazer uma requisição de rede, utilizamos a função fetch, que é responsável por iniciar a comunicação com o recurso desejado.
let promise = fetch(url, [options])
O parâmetro url representa o endereço para o qual a requisição será enviada, enquanto options é um objeto opcional que permite configurar detalhes da requisição (como método HTTP, cabeçalhos, corpo, entre outros — veremos isso mais adiante). A Promise retornada pela função fetch será resolvida com um objeto do tipo Response, que contém propriedades e métodos úteis para lidar com a resposta. Entre os mais importantes, destacam-se:
- status: Atributo que contém o código de status HTTP da resposta. Ex.:
200,201,404. - statusText: Atributo com a descrição textual do status HTTP. Ex.:
OK,Created,Not Found. - ok: Atributo booleano que será
truese o status da resposta estiver entre200e299, indicando sucesso. - headers: Objeto do tipo
Headerscontendo os cabeçalhos da resposta. É possível obter o valor de um cabeçalho específico usando o métodoget(nome-do-header). Ex.:response.headers.get('Content-Type'). - text(): Método que retorna uma Promise resolvida com o corpo da resposta em formato de texto.
- json(): Método que retorna uma Promise resolvida com o corpo da resposta convertido automaticamente para um objeto JavaScript (JSON).
Vamos ver agora um exemplo simples de como utilizar a função fetch para realizar uma requisição que recupera uma lista de usuários e insere o conteúdo dentro de uma div com o ID "output".
const output = document.querySelector('#output');
let promise = fetch('data/usuarios.php');
promise.then( (response) =>{
if(response.ok){
return response.json();
}
throw new Error('Não foi possível recuperar a lista de usuários');
})
.then( (json) =>{
json.forEach(element => {
output.innerHTML += `<p>${element.nome} - ${element.email} - ${element.username}</p>`;
});
})
.catch ((error) => {
output.innerHTML = `<p>${error.message}</p>`;
});
No código acima, começamos selecionando o elemento onde será inserido o conteúdo retornado pela requisição. Em seguida, utilizamos a função fetch (linha 3) para fazer uma requisição à URL "data/usuarios.php", que deverá retornar dados em formato JSON. A função fetch retorna uma promise, sobre a qual aplicamos o método then (linha 4) para definir um callback a ser executado quando a requisição for concluída com sucesso. Esse callback recebe como parâmetro um objeto response, que representa a resposta da requisição. Na linha 5, verificamos se a resposta foi bem-sucedida (ou seja, se o status code foi de 200-299) utilizando a propriedade ok. Se for o caso, chamamos o método json() (linha 6) do objeto response, que converte o corpo da resposta em um objeto Javascript e também retorna uma nova promise. Caso a resposta não seja válida (status fora do intervalo 200-299), uma exceção é lançada na linha 8.
Como o método json() chamado na linha 6 retorna uma nova Promise, precisamos encadear outro then para tratar sua resolução. Nesse segundo then, recebemos como parâmetro o objeto Javascript já convertido a partir da resposta JSON (que, neste caso, é um array de objetos). Em seguida, usamos um laço forEach para percorrer cada item do array, extraímos as informações relevantes de cada objeto e montamos uma string HTML com esses dados, que é então adicionada ao conteúdo do elemento output.
Na linha 15, utilizamos o método catch, passando uma função de callback responsável por tratar o estado de rejeição da promise. Essa função também captura e trata qualquer exceção que possa ter sido lançada dentro das funções de callback utilizadas nos métodos then.
Para visualizar os dados enviados e recebidos — como payload e cabeçalhos (headers) — em uma requisição feita com a função fetch, abra as “Ferramentas do programador” pressionando F12 ou Ctrl + Shift + I no Chrome. Em seguida, vá até a aba Network, onde serão listadas todas as requisições realizadas pela página. Ao clicar em uma delas, será exibido à direita um painel com várias abas contendo os headers da requisição e da resposta, o payload (caso exista) e o conteúdo da resposta.

Para visualizar o exemplo em funcionamento, acesse esta página e abra as “Ferramentas do programador” para visualizar as informações da requisição.
No exemplo anterior, foi realizada uma requisição do tipo GET — que é o método padrão utilizado pela função fetch quando nenhum objeto de configuração (options) é fornecido.
Agora vamos realizar uma requisição utilizando o método POST, para entendermos melhor como configurar o objeto options que pode ser passado como segundo parâmetro para a função fetch.
const output = document.querySelector('#output');
const options = {
method : 'POST',
body: `{
"nome": "Fulano da Silva",
"email": "fulano@exemplo.com",
"username": "fsilva"
}`,
headers:{
"Accept": "application/json",
"Content-Type": "application/json"
}
};
let promise = fetch('data/usuarios-create.php', options);
output.innerHTML = 'Processando...'
promise.then( (response) =>{
if(response.status === 201){
return response.json();
}
throw new Error('Não foi possível criar o usuários');
})
.then( (json) =>{
output.innerHTML = `<div>
<div><strong>ID</strong>: ${json.id} </div>
<div><strong>Nome</strong>: ${json.nome} </div>
<div><strong>E-mail</strong>: ${json.email} </div>
<div><strong>Username</strong>: ${json.username} </div>
</div>`;
})
.catch ((error) => {
output.innerHTML = `<p>${error.message}</p>`;
});
A principal novidade deste exemplo está na criação do objeto options na linha 3, que é passado como segundo argumento para a função fetch na linha 16. Na linha 19, verificamos se o código de status HTTP retornado é 201 (Created), o que indica que a requisição foi concluída com sucesso. Embora, no exemplo, tenhamos montado manualmente uma string JSON, também poderíamos utilizar a função JSON.stringify(obj) para converter um objeto Javascript em uma string JSON automaticamente, inclusve é o recomendado.
O objeto options pode conter diversos atributos que controlam o comportamento da requisição. A seguir, destacamos os mais utilizados:
method: Define o método HTTP da requisição, comoGET,POST,PUT,DELETE, entre outros.body: Representa o corpo da requisição. Pode ser uma string (geralmente no formato JSON), um objetoFormData, umBlob, ou outros tipos suportados, dependendo doContent-Type.headers: Um objeto com os cabeçalhos (headers) da requisição. Exemplos comuns incluemAccept: text/plain,Content-Type: application/jsoneAuthorization: <TOKEN>. Pode ser informado através de um objeto simples ou um objetoHeader.
Você pode conferir o exemplo clicando aqui. Não se esqueça de abrir as “Ferramentas de Desenvolvedor” para visualizar a requisição.
Embora o formato JSON seja o mais utilizado ao enviar dados com a função fetch, também é possível enviar as informações no formato tradicional de formulários. Para isso, basta definir o cabeçalho Content-Type com o valor application/x-www-form-urlencoded e fornecer no body uma string com os dados codificados no formato chave=valor, separados por &.
field1=value1&field2=value2
A seguir, veja como deve ser estruturado o objeto options para enviar dados no formato application/x-www-form-urlencoded:
const options = {
method : 'POST',
body: `nome=fulano&email=fulano@exemplo.com&username=fsilva`,
headers:{
"Content-Type": "application/x-www-form-urlencoded"
}
};
Também é possível utilizar o objeto FormData para enviar dados com a função fetch. Ele permite tanto adicionar os campos manualmente quanto preencher os dados automaticamente a partir de um elemento <form> do HTML, facilitando bastante o envio de formulários.
Para adicionar os campos manualmente, criamos uma instância de FormData com new FormData() e usamos o método append para incluir cada par campo-valor individualmente.
const data = new FormData();
data.append('nome', document.querySelector("input[name='nome']").value);
data.append('email', document.querySelector("input[name='email']").value);
data.append('username', document.querySelector("input[name='username']").value);
const options = {
method : 'POST',
body: data,
headers:{
"Accept": "application/json",
}
};
No exemplo a anterior, os dados dos campos nome, email e username são extraídos diretamente dos inputs de um formulário e adicionados ao objeto FormData. Em seguida, usamos esse objeto como corpo da requisição POST ao configurar a função fetch. Note que, nesse caso, não é necessário definir o cabeçalho Content-Type manualmente — o próprio navegador se encarrega de definir o tipo correto (multipart/form-data) com o boundary adequado
Se quisermos adicionar automaticamente todos os campos de um formulário, basta passar o elemento <form> diretamente para o construtor de FormData. Isso permite capturar todos os dados do formulário de forma simples e eficiente, sem precisar adicionar cada campo manualmente.
const form = document.querySelector('#form');
const data = new FormData(form);
const options = {
method : 'POST',
body: data,
headers:{
"Accept": "application/json",
}
};
Acesse o exemplo para ver como usar FormData com um formulário. Não se esqueça de inspecionar a requisição via “Ferramentas do Programador”.
Upload de arquivos com Fetch
Para realizar o upload de um arquivo com fetch, podemos usar o objeto FormData e adicionar o arquivo selecionado a partir de um campo <input type="file">. No exemplo abaixo, utilizamos inputFile.files[0] para acessar o primeiro arquivo escolhido pelo usuário e adicioná-lo ao FormData com o método append
const data = new FormData();
const inputFile = document.querySelector("#file");
data.append('file', inputFile.files[0]);
const options = {
method : 'POST',
body: data,
headers:{
"Accept": "application/json",
}
};
Acesse o exemplo para visualizar o exemplo de upload de arquivo com o fetch.
Esta foi uma introdução aos conceitos básicos de Promises e da API Fetch — dois recursos fundamentais para quem desenvolve aplicações web modernas. Para se aprofundar, consulte a documentação do Fetch API e das Promises. T++
