As promessas são um mecanismo central para lidar com código assíncrono em JavaScript. Você os encontrará em muitas bibliotecas e estruturas JavaScript, onde são usados para gerenciar os resultados de uma ação. O fetch()
API é um exemplo de promessas em ação. Como desenvolvedor, você pode não estar familiarizado com a criação e uso de promessas fora de um produto existente, mas é surpreendentemente simples. Aprender como criar promessas ajudará você a entender como as bibliotecas as utilizam. Ele também coloca à sua disposição um poderoso mecanismo de programação assíncrona.
Programação assíncrona com promessas
No exemplo a seguir, estamos usando um Promise
para lidar com os resultados de uma operação de rede. Em vez de fazer uma chamada de rede, usamos apenas um tempo limite:
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const data = "This is the fetched data!";
resolve(data);
}, 2000);
});
}
const promise = fetchData();
promise.then((data) => {
console.log("This will print second:", data);
});
console.log("This will print first.");
Neste código, definimos um fetchData()
função que retorna um Promise
. Chamamos o método e mantemos o Promise
no promise
variável. Então usamos o Promise.then()
método para lidar com os resultados.
A essência deste exemplo é que o fetchData()
chamada acontece imediatamente no fluxo de código, enquanto o retorno de chamada passado para then()
só acontece após a conclusão da operação assíncrona.
Se você olhar para dentro fetchData()
você verá que ele define um Promise
objeto, e esse objeto assume uma função com dois argumentos: resolve
e reject
. Se o Promise
consegue, chama resolve
; se houver algum problema, ele liga reject
. No nosso caso, simulamos o resultado de uma chamada de rede chamando resolve
e retornando uma string.
Muitas vezes você verá o Promise
chamado e tratado diretamente, assim:
fetchData().then((data) => {
console.log("This will print second:", data);
});
Agora vamos pensar nos erros. Em nosso exemplo, podemos simular uma condição de erro:
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() < 0.5) {
reject("An error occurred while fetching data!");
} else {
const data = "This is the fetched data!";
resolve(data);
}
}, 2000);
});
}
Cerca de metade das vezes, a promessa neste código apresentará um erro ao chamar reject()
. Em um aplicativo do mundo real, isso poderia acontecer se a chamada de rede falhasse ou se o servidor retornasse um erro. Para lidar com a possibilidade de falha ao ligar fetchData()
nós usamos catch()
:
fetchData().then((data) => {
console.log("That was a good one:", data);
}).catch((error) => {
console.log("That was an error:", error)
});
Se você executar esse código várias vezes, obterá uma mistura de erros e acertos. Resumindo, é uma maneira simples de descrever seu comportamento assíncrono e depois consumi-lo.
Cadeias de promessas em JavaScript
Uma das belezas das promessas é que você pode encadeá-las. Isso ajuda a evitar retornos de chamada profundamente aninhados e simplifica o tratamento de erros assíncronos aninhados. (Não vou turvar as águas mostrando uma função JavaScript antiquada com retorno de chamada de argumento. Acredite na minha palavra, isso fica confuso.)
Deixando nosso fetchData()
funcionar como está, vamos adicionar um processData()
função. O processData()
função depende dos resultados de fetchData()
. Agora, poderíamos agrupar a lógica de processamento dentro da chamada de retorno de fetchData()
mas as promessas nos permitem fazer algo muito mais limpo:
function processData(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const processedData = data + " - Processed";
resolve(processedData);
}, 1000);
});
}
fetchData()
.then((data) => {
console.log("Fetched data:", data);
return processData(data);
})
.then((processedData) => {
console.log("Processed data:", processedData);
})
.catch((error) => {
console.error("Error:", error);
});
Se você executar esse código várias vezes, notará que quando fetchData()
consegue, ambos then()
métodos são chamados corretamente. Quando fetchData()
falha, toda a cadeia entra em curto-circuito e o final catch()
são chamados. Isso é semelhante ao funcionamento dos blocos try/catch.
Se colocássemos o catch()
depois do primeiro then()
seria responsável apenas por fetchData()
erros. Neste caso, nosso catch()
cuidará tanto do fetchData()
e processData()
erros.
A chave aqui é que fetchData()
de then()
manipulador retorna a promessa de processData(data)
. É isso que nos permite encadeá-los.
Execute não importa o que aconteça: Promise.finally()
Assim como try/catch lhe dá uma finally()
, Promise.finally()
será executado independentemente do que acontecer na cadeia de promessas:
fetchData()
.then((data) => {
console.log("Fetched data:", data);
return processData(data);
})
.then((processedData) => {
console.log("Processed data:", processedData);
})
.catch((error) => {
console.error("Error:", error);
})
.finally(() => {
console.log("Cleaning up.");
})
O finally()
é útil quando você precisa fazer algo, não importa o que aconteça, como encerrar uma conexão.
Falha rápido: Promise.all()
Agora vamos considerar uma situação em que precisamos fazer diversas chamadas simultaneamente. Digamos que precisamos fazer duas solicitações de rede e precisamos dos resultados de ambas. Se qualquer um deles falhar, queremos falhar em toda a operação. Nossa abordagem de encadeamento acima poderia funcionar, mas não é ideal porque exige que uma solicitação termine antes do início da próxima. Em vez disso, podemos usar Promise.all()
:
Promise.all((fetchData(), fetchOtherData()))
.then((data) => { // data is an array
console.log("Fetched all data:", data);
})
.catch((error) => {
console.error("An error occurred with Promise.all:", error);
});
Como o JavaScript é de thread único, essas operações não são verdadeiramente simultâneas, mas estão muito mais próximas. Em particular, o mecanismo JavaScript pode iniciar uma solicitação e depois iniciar a outra enquanto ainda está em andamento. Essa abordagem nos deixa o mais próximo possível da execução paralela com JavaScript.
Se alguma das promessas fosse passada para Promise.all()
falhar, interromperá toda a execução e irá para o fornecido catch()
. Dessa forma, Promise.all()
é “falhar rápido”.
Você também pode usar finally()
com Promise.all()
e ele se comportará conforme o esperado, funcionando independentemente do resultado do conjunto de promessas.
No then()
método, você receberá um array, com cada elemento correspondente à promessa passada, assim:
Promise.all((fetchData(), fetchData2()))
.then((data) => {
console.log("FetchData() = " + data(0) + " fetchMoreData() = " + data(1) );
})
Deixe o mais rápido vencer: Promise.race()
Às vezes você tem várias tarefas assíncronas, mas só precisa da primeira para ter sucesso. Isso pode acontecer quando você tem dois serviços redundantes e deseja usar o mais rápido.
Digamos fetchData()
e fetchSameData()
existem duas maneiras de solicitar as mesmas informações e ambas retornam promessas. Veja como usamos race()
para gerenciá-los:
Promise.race((fetchData(), fetchSameData()))
.then((data) => {
console.log("First data received:", data);
});
Neste caso, o then()
o retorno de chamada receberá apenas um valor para dados – o valor de retorno do vencedor (mais rápido) Promise
.
Os erros são ligeiramente matizados com race()
. Se o rejeitado Promise
é o primeiro a acontecer, então toda a corrida termina e catch()
é chamado. Se a promessa rejeitada acontecer após outra promessa ter sido resolvida, o erro será ignorado.
Tudo ou nada: Promise.allSettled()
Se você quiser aguardar a conclusão de uma coleção de operações assíncronas, independentemente de elas falharem ou serem bem-sucedidas, você pode usar allSettled()
. Por exemplo:
Promise.allSettled((fetchData(), fetchMoreData())).then((results) =>
results.forEach((result) => console.log(result.status)),
);
O results
argumento passado para o then()
manipulador conterá um array descrevendo os resultados das operações, algo como:
(0: {status: 'fulfilled', value: "This is the fetched data!"},
1: {status: 'rejected', reason: undefined})
Então você obtém um campo de status que é fulfilled
ou rejected
. Se for cumprido (resolvido), então o valor conterá o argumento chamado por resolve()
. Promessas rejeitadas preencherão o reason
campo com a causa do erro, assumindo que uma foi fornecida.
Em breve: Promise.withResolvers()
A especificação ECMAScript 2024 inclui um método estático em Promise
chamado withResolvers()
. A maioria dos navegadores e ambientes do lado do servidor já oferecem suporte a isso. É um pouco esotérico, mas a Mozilla tem um bom exemplo de como é usado. O novo método permite que você declare um Promise
juntamente com o resolve
e reject
funciona como variáveis independentes, mantendo-as no mesmo escopo.
Conclusão
As promessas são um aspecto importante e útil do JavaScript. Eles podem fornecer a ferramenta certa em uma variedade de situações de programação assíncrona e aparecem o tempo todo ao usar estruturas e bibliotecas de terceiros. Os elementos abordados neste tutorial são todos componentes de alto nível, portanto, é uma API bastante simples de conhecer.