Promises ainda são um mistério para muita gente no JS, e apesar de já estarem na linguagem faz muito tempo, o tratamento de erros nelas ainda é algo que pode confundir muitas pessoas, por isso hoje vamos explorar o fluxo dos dados nas promises nesses tipos de situações.
A situação mais simples é lidar utilizando o then para os caminhos felizes, e o catch para os de erro, ex:
const randomInt = (max) => Math.floor(Math.random() * max);
const Result = { success: (message) => Promise.resolve(message), error: (message) => Promise.reject(message), random: () => { return randomInt() === 1 ? Result.success('success') : Result.error('error'); }
}; Result.random() .then((success) => console.log(success)) .catch((error) => console.error(error));
Até aqui nada de novo, o then/catch age como um if/else, onde o then seria o if, e o catch o else. Algo que talvez você não conheça seja o Promise.resolve
e o Promise.reject
, essas são duas funções que servem para criar promises, só que ao invés de termos que instânciar e fazer todo o setup do construtor da promise, o Promise.resolve
já devolve uma promise resolvida com sucesso, e o Promise.reject
devolve uma promise rejeitada, assim podemos testar mais fácil esses fluxos.
Para ficar mais fácil de visualizar, o Promise.resolve
seria equivalente a esse código aqui:
function resolve(value) { return new Promise((resolve) => resolve(value));
}
e o Promise.reject
a este código aqui:
function reject(value) { return new Promise((_, reject) => reject(value));
}
Uma das grandes inovações nas Promises do JS foi a sintaxe de async/await. onde ao invés desse jeito meio estranho de lidar com as coisas utilizando funções encadeadas, nós podemos escrever código que usa as promises de uma forma mais familiar para quem está acostumado a programar de forma estruturada. Por exemplo, o código do exemplo anterior poderia ser feito da seguinte maneira:
const randomInt = (max) => Math.floor(Math.random() * max); const Result = { success: async (message) => message, error: async (message) => { throw message; }, random: () => { return randomInt() === 1 ? Result.success('success') : Result.error('error'); }
}; async function app() { try { const success = await Result.random(); console.log(success); } catch (error) { console.error(error); }
} app();
Uma função async permite que nós utilizemos o await
para esperar uma promise resolver, e então pegar o valor contido dentro dela, caso a promise seja rejeitada, podemos pegar o valor rejeitado por meio de um try catch, no bloco catch.
Como uma função async vai envolver o resultado retornado numa promise caso ele já não seja uma promise, então podemos simplificar o Promise.resolve
para uma função async que recebe um valor, e o devolve logo em seguida.
Já no caso do Promise.reject
podemos fazer algo equivalente usando o throw que, ao ser utilizado dentro de uma função no contexto de uma promise, rejeita a promise, e gera uma promise rejeitada com o valor lançado pelo throw
.
É muito comum que o processamento de um valor contido numa promise seja realizado ao longo de vários then/catch encadeados, por exemplo:
fetch("/api/resource") .then(response => response.json()) .then(resource => { // fazer algo com o resource });
Porém, neste caso, onde colocaríamos o catch? A resposta para isso é que ele pode ser colocado ao final de todos os then, pois qualquer promise rejeitada, vai jogar a bola para a próxima até ter alguém com um catch definido, que ai sim vai tratar esse erro, semelhante ao padrão chain of responsability da orientação a objetos. Ex:
fetch("/api/resource") .then(response => response.json()) .then(resource => { // fazer algo com o resource }) .catch(error => { // fazer algo com o erro });
Nesse caso, se a promise retornada pelo fetch for rejeitada, ela vai ser passada para o primeiro then, que não tem um catch, e então vai ser passada para o segundo then, que também não é um catch, e ai sim vai ser passada para o catch, que é um catch obviamente, para ser tratada.
Da mesma lógica, se ela for resolvida com sucesso, ela vai ser passada para o primeiro then, que vai tratar ela e devolver um valor, se ele também for resolvido, então o resultado dele vai ser passado para o segundo then, que se também for resolvido, vai passar para o catch, que dessa vez não fará nada.
Caso algum desses then acabe por ser rejeitado também por algum motivo, por exemplo lançar um erro com throw ou retornar uma promise rejeitada usando Promise.reject
, ai daquele ponto em diante esse erro vai viajar pelos próximos then/catch igual explicado anteriormente, sendo ignorado até encontrar um catch, e então será tratado por ele (inclusive é por isso que não tem como tipar o valor do catch, pois ele pode vir de qualquer ponto antes desse catch, além de oder ser qualquer coisa).
Ainda no exemplo de um fetch, outra coisa bem comum é validar o status code da resposta para saber se a requisição foi bem sucedida ou não, e no caso dela não ser rejeitar aquele then para entrar num catch. Ex:
fetch("/api/resource") .then(response => { if (![200, 201, 203].includes(response.status)) { throw response; } return response.json(); }) .then(resource => { // fazer algo com o resource });
Isso é necessário pois uma promise gerada pelo fetch só rejeita por erro de rede, então se cair a conexão no meio da requisição, ou enviar para uma URL que não existe (aqui teria que ser um domínio que não existe, e não uma rota), ela seria rejeitada, agora por erros da API em si, a resposta seria devolvida pelo servidor, e a promise seria resolvida mesmo em caso de “erro”, logo, como nós sabemos quais códigos representam erro e quais não, podemos implementar isso manualmente para promise rejeitar em caso de erro também, e é isso que o if dentro do then faz.
Dito isso, seria interessante que pudessemos tratar cada erro desses individualmente enquanto mantemos o catch no final de tudo como uma segurança para qualquer erro inesperado, e como o async/await é o uso mais popular quando se trata de promises, muitas programadores poderiam pensar em fazer dessa forma aqui, e não estaria errado:
try { const response = await fetch("/api/resource"); try { if (![200, 201, 203].includes(response.status)) { throw response; } try { const resource = await response.json(); } catch (invalidJsonError) { console.log(invalidJsonError); } } catch (apiError) { console.log(apiError); }
} catch (networkError) { console.log(networkError)
}
Entretanto podemos ver que o código não fica a coisa mais legível do mundo, lembrando um hadouken code:
Aqui que usar o clássico then/catch pode acabar se pagando, pois podemos colocar um catch após cada função que gera uma promise, e lidar com o erro que ela gera primeiro, e depois processar o caminho feliz:
fetch("/api/resource") .catch((networkError) => { console.log(networkError) throw networkError; }) .then(response => { if (![200, 201, 203].includes(response.status)) { throw response; } return response; }) .catch(apiError => { console.log(apiError) throw apiError; }) .then(response => response.json()) .catch(invalidJsonError => { console.log(invalidJsonError) throw invalidJsonError; }) .then(resource => { // fazer algo com o resource });
Aqui a promise é gerada pelo fetch
, então nós tratamos um possível erro nela (que seria o de rede) com o primeiro catch, e o possível caminho feliz dela com o primeiro then, em seguida caso seja o then que foi executado, uma nova promise vai ser gerada, então nós pegamos o erro dela com o segundo catch, e tratamos o sucesso dela com o segundo then, e a lógica se repete daí em diante.
É interessante notar que esse código tem uma melhora em relação a versão async/await pois agora os níveis de identação se mantém uniformes ao longo de toda a cadeia ao invés de irem aumentando conforme novos erros forem aparecendo, mas ainda pode ser melhor, e é isso que abordaremos na seção final deste artigo.
Apesar de geralmente serem vistos em separado na maioria dos tutoriais (o que faz sentido considerando que se você quer falar sobre um assunto, focar só nele pode ser uma boa ideia), não é como se você não pudesse utilizar os dois ao mesmo tempo, afinal o await
espera uma promise ser resolvida, e extrai o seu valor, enquanto o then
/catch
geram novas promises, o que significa que você pode dar um await
numa promise devolvida por um deles, dessa forma transformando os dados antes do await
fazer efeito. Ex:
const value = await Promise.resolve(1).then(x => x + 1);
console.log(value); // 2 const error = await Promise.reject(0).catch(x => x + 1);
console.log(error); // 1
Uma outra coisa a se notar aqui, é que quando um valor é retornado de um catch, a promise criada é uma resolvida com sucesso, e não uma nova rejeitada, pois entende-se que você conseguiu se recuperar do erro e está pronto para continuar no caminho feliz, e por isso que nos exemplos do nosso artigo anterior, os catchs continham um throw
, pois lançar um valor no contexto de uma promise, significa que ela foi rejeitada, e por isso uma nova promise rejeitada seria criada nesse caso.
Dessa forma, podemos utilizar uma abordagem no qual utilizamos um valor (um array de duas casas, um objeto literal, um objeto customizado, e etc) para conseguir representar tanto valores de sucesso quanto valores de erro ao mesmo tempo, assim ao conseguirmos dar o await
no resultado, nós teremos ambas informações ao mesmo tempo.
Aqui irei representar esse valor como um array de duas casas, a primeira sendo o erro, e a segunda sendo o valor, com uma das duas sendo null, ex:
[null, 'valor de sucesso'] // em caso de sucesso
['valor de erro', null] // em caso de erro
Assim podemos retornar um array no then, o outro no catch, e manter um resultado do mesmo tipo sendo passado pelo await independente de cair no then ou no catch, ex:
const [networkError, response] = await fetch("/api/resource") .then(response => [null, response]) .catch(error => [error, null]); if (networkError && ![200, 201, 203].includes(response.status)) { // tratar erro de api
} const [invalidJsonError, resource] = await response .json() .then(json => [null, json]) .catch(error => [error, null]);
Note como o código ficou bem mais simples assim, e isso pode ficar ainda melhor, podemos abstrair a criação desses arrays em uma função, e deixar o código ainda mais limpo:
const toResult = (promise) => { return promise .then((value) => [null, value]) .catch((error) => [error, null]);
}; const [networkError, response] = await toResult(fetch("/api/resource")); const isError = !!networkError;
const isApiError = isError && ![200, 201, 203].includes(response.status); if (isApiError) { // tratar erro de api
} const [invalidJsonError, resource] = await toResult(response.json());
Espero que o artigo de hoje tenha aberto sua mente para as várias possibilidades de tratamento de erros que as promises nos oferecem e que alguma dessas estratégias sirva para o seu dia-a-dai. Não se esqueça de compartilhar este artigo, e até a próxima.
ASS: Suporte cansado.