RxJs Tratamento de Erros: Guia Prático Completo

O manuseio de erros é uma parte essencial do RxJs, pois precisaremos dele em praticamente qualquer programa reativo que escrevemos.

O manuseio de erros no RxJS provavelmente não é tão bem entendido quanto outras partes da biblioteca, mas na verdade é bastante simples de entender se nos concentrarmos em entender primeiro o contrato Observável em geral.

Neste post, vamos fornecer um guia completo contendo as estratégias mais comuns de tratamento de erros que você precisará para cobrir a maioria dos cenários práticos, começando com o básico (o contrato Observável).

Tabela de Conteúdos

Neste post, vamos cobrir os seguintes tópicos:

  • O Contrato Observável e Tratamento de Erros
  • Assinatura de RxJs e chamadas de erro
  • O Operador de ApanharErro
  • O Apanhar e Substituir Estratégia
  • >

  • Estratégia de lançamento e de captura e de re-lançamento
  • >

  • Usar o catchError várias vezes numa cadeia observável
  • >

  • O operador finalizador
  • >

  • O novo lançamento Estratégia
  • Então, repetir Quando o operador
  • Criar uma Notificação Observável
  • Estratégia de Tentativa de Retorno Imediato
  • Estratégia de Tentativa de Retorno Atrasado
  • Atraso Quando Operador
  • O temporizador Função de criação observável
  • Running Github repository (with code samples)
  • Conclusions

So sem mais delongas, vamos começar com o nosso RxJs Error Handling deep dive!

O Contrato Observável e Tratamento de Erros

Para compreender o tratamento de erros em RxJs, precisamos primeiro de compreender que um determinado fluxo só pode errar uma vez. Isto é definido pelo contrato Observável, que diz que um fluxo pode emitir zero ou mais valores.

O contrato funciona assim porque é assim que todos os fluxos que observamos no nosso runtime funcionam na prática. As solicitações de rede podem falhar, por exemplo.

Um fluxo também pode completar, o que significa que:

  • o fluxo terminou seu ciclo de vida sem nenhum erro
  • após a conclusão, o fluxo não emitirá mais nenhum valor

Como uma alternativa à conclusão, um fluxo também pode errar, o que significa que:

  • o fluxo terminou o seu ciclo de vida com um erro
  • após o erro ser lançado, o fluxo não emitirá quaisquer outros valores

Notificar que a conclusão ou erro são mutuamente exclusivos:

  • Se o fluxo terminar, não pode emitir nenhum outro erro depois
  • Se os erros do fluxo saírem, não pode terminar depois

Notificar também que não há nenhuma obrigação para o fluxo terminar ou erro fora, essas duas possibilidades são opcionais. Mas apenas uma dessas duas possibilidades pode ocorrer, não ambas.

Isto significa que quando um determinado stream erra, não podemos mais utilizá-lo, de acordo com o contrato Observável. Você deve estar pensando neste ponto, então como podemos recuperar de um erro?

RxJs subscribe and error callbacks

Para ver o comportamento de manipulação de erros RxJs em ação, vamos criar um stream e subscribe to it. Vamos lembrar que a chamada de subscrição leva três argumentos opcionais:

  • uma função manipuladora de erros, que é chamada cada vez que o stream emite um valor
  • uma função manipuladora de erros, que só é chamada se ocorrer um erro. Este manipulador recebe o erro em si
  • uma função manipuladora de conclusão, que só é chamada se o stream completar

Exemplo de Comportamento de Conclusão

Se o stream não sair com erro, então isto é o que veríamos no console:

HTTP response {payload: Array(9)}HTTP request completed.

Como podemos ver, este stream HTTP emite apenas um valor, e então ele completa, o que significa que não ocorreram erros.

Mas o que acontece se o stream lançar um erro em seu lugar? Nesse caso, veremos o seguinte no console ao invés:

RxJs Error Handling console output

Como podemos ver, o stream não emitiu nenhum valor e imediatamente errou. Após o erro, não ocorreu nenhuma conclusão.

Limitações do gerenciador de erros de subscribe

Errodos de manipulação usando a chamada subscribe é às vezes tudo que precisamos, mas esta abordagem de manipulação de erros é limitada. Usando esta abordagem, não podemos, por exemplo, recuperar do erro ou emitir um valor alternativo de fallback que substitui o valor que estávamos esperando do backend.

Vamos então aprender alguns operadores que nos permitirão implementar algumas estratégias mais avançadas de tratamento de erros.

O operador catchError

Na programação síncrona, temos a opção de envolver um bloco de código em uma cláusula de tentativa, capturar qualquer erro que ele possa lançar com um bloco catch e então lidar com o erro.

Aqui está o aspecto da sintaxe de captura síncrona:

Este mecanismo é muito poderoso porque podemos lidar num só lugar com qualquer erro que aconteça dentro do bloco try/catch.

O problema é que, em Javascript muitas operações são assíncronas, e uma chamada HTTP é um desses exemplos onde as coisas acontecem de forma assíncrona.

RxJs nos fornece algo próximo a esta funcionalidade, através do operador catchError do RxJs.

Como funciona o catchError?

Como de costume e como com qualquer operador RxJs, catchError é simplesmente uma função que aceita uma entrada Observável, e emite uma Saída Observável.

Com cada chamada para catchError, precisamos passar a ela uma função que chamaremos de função de tratamento de erros.

O operador catchError pega como input um Observable que pode sair por erro, e começa a emitir os valores do input Observable no seu output Observable.

Se não ocorrer erro, o output Observable produzido por catchError funciona exatamente da mesma forma que o input Observable.

O que acontece quando um erro é lançado?

No entanto, se ocorrer um erro, então a lógica catchError vai entrar em ação. O operador catchError vai pegar o erro e passá-lo para a função de tratamento de erros.

Essa função deverá retornar um Observable que será um Observable substituto para o fluxo que acabou de errar.

Lembramos que o fluxo de entrada do catchError está errado, então de acordo com o contrato do Observable não podemos mais usá-lo.

Esta substituição do Observable será então subscrita e seus valores serão usados no lugar da entrada do Observable com erro.

A estratégia de captura e substituição

Demos um exemplo de como catchError pode ser usado para fornecer um substituto Observável que emite valores fallback:

Dividamos a implementação da estratégia de captura e substituição:

  • estamos passando ao operador do catchError uma função, que é a função de tratamento de erros
  • a função de tratamento de erros não é chamada imediatamente, e em geral, normalmente não é chamada
  • somente quando ocorre um erro na entrada Observable do catchError, a função de tratamento de erros será chamada
  • se ocorrer um erro no fluxo de entrada, esta função é então retornando um Observável construído usando a função of()
  • a função of() constrói um Observável que emite apenas um valor () e depois completa
  • a função de tratamento de erros retorna a recuperação do Observável (of()), que é subscrito pelo operador catchError
  • os valores da recuperação Observable são então emitidos como valores de substituição na saída Observable retornado por catchError

Como resultado final, o http$ Observable não irá mais errar! Aqui está o resultado que obtemos na consola:

HTTP response HTTP request completed.

Como podemos ver, o erro de manipulação de retorno da chamada em subscribe() já não é invocado. Em vez disso, aqui está o que acontece:

  • o valor do array vazio é emitido
  • o http$O Observável é então completado

Como podemos ver, o Observável de substituição foi usado para fornecer um valor padrão de fallback () aos subscritores de http$, apesar do fato de que o Observável original cometeu erro.

Nota que também poderíamos ter adicionado alguma manipulação de erro local, antes de devolver a substituição do Observável!

E isto cobre a Estratégia de Captura e Substituição, agora vamos ver como podemos também usar o catchError para rethrow o erro, em vez de fornecer valores de fallback.

A Estratégia de Captura e Reposição

Comecemos por reparar que o Observável de substituição fornecido através do catchError também pode, como qualquer outro Observável.

E se isso acontecer, o erro será propagado para os subscritores da saída Observable do catchError.

Este comportamento de propagação de erro nos dá um mecanismo para rethrow o erro capturado pelo catchError, depois de manipular o erro localmente. Podemos fazê-lo da seguinte forma:

Descaracterização da captura e do re-lançamento

Dividamos passo a passo a implementação da Estratégia de Captura e Retirada:

  • tal como antes, estamos capturando o erro, e retornando uma substituição Observável
  • mas desta vez, ao invés de fornecer um valor de saída de substituição como , estamos agora manipulando o erro localmente na função catchError
  • neste caso, estamos simplesmente registrando o erro no console, mas poderíamos adicionar qualquer lógica local de tratamento de erros que quiséssemos, como por exemplo mostrar uma mensagem de erro ao utilizador
  • Estamos então a devolver um Observable substituto que desta vez foi criado usando throwError
  • throwError cria um Observable que nunca emite qualquer valor. Em vez disso, ele erra imediatamente usando o mesmo erro capturado pelo catchError
  • isto significa que o output Observable do catchError também erra com exatamente o mesmo erro lançado pela entrada do catchError
  • isto significa que nós conseguimos rethrow com sucesso o erro lançado inicialmente pelo input Observable do catchError para seu output Observable
  • o erro pode agora ser tratado mais adiante pelo resto da cadeia Observable, se necessário

Se agora executarmos o código acima, aqui está o resultado que obtemos no console:

RxJs Error Handling console output

Como podemos ver, o mesmo erro foi registrado tanto no bloco catchError quanto na função de controle de erros de assinatura, como esperado.

Usando catchError várias vezes em uma cadeia Observável

Nota que podemos usar catchError várias vezes em diferentes pontos da cadeia Observável, se necessário, e adotar diferentes estratégias de erro em cada ponto da cadeia.

Podemos, por exemplo, apanhar um erro para cima na cadeia Observável, manipulá-lo localmente e re-lançá-lo, e depois mais abaixo na cadeia Observável podemos apanhar o mesmo erro novamente e desta vez fornecer um valor de retorno (em vez de re-lançá-lo):

Se executarmos o código acima, este é o output que obtemos no console:

RxJs Map Operator marble diagram

Como podemos ver, o erro foi de fato repelido inicialmente, mas nunca chegou à função subscribe error handler. Em vez disso, o fallback valor foi emitido, como esperado.

O Operador Finalize

Beside um bloco de captura para lidar com erros, a sintaxe síncrona do Javascript também fornece um bloco final que pode ser usado para executar código que queremos sempre executado.

O bloco final é tipicamente usado para liberar recursos caros, como por exemplo, fechar conexões de rede ou liberar memória.

Não parecido com o código no bloco catch, o código no bloco finally será executado independentemente se um erro for lançado ou não:

RxJs nos fornece um operador que tem um comportamento similar ao da funcionalidade finally, chamado Operador finalize.

Nota: não podemos chamá-lo de operador final, pois finalmente é uma palavra-chave reservada em Javascript

Finalize Operator Example

Apenas como o operador catchError, podemos adicionar múltiplas chamadas finais em diferentes lugares da cadeia Observável, se necessário, para garantir que os múltiplos recursos sejam liberados corretamente:

Vamos agora executar este código, e ver como os múltiplos blocos finalizados estão sendo executados:

RxJs Saída do console de tratamento de erros

Note que o último bloco finalizado é executado após as funções de controle de valores subscritores e de controle de conclusão.

A Estratégia de Retry

Como uma alternativa para re-inscrever o erro ou fornecer valores de fallback, também podemos simplesmente tentar de novo subscrever o Observável errôneo.

Lembramos, uma vez que os erros do stream não podemos recuperá-lo, mas nada nos impede de subscrever novamente o Observável do qual o stream foi derivado, e criar outro stream.

Aqui está como isto funciona:

  • vamos pegar no input Observável, e subscrevê-lo, o que cria um novo stream
  • se esse stream não errar, vamos deixar os seus valores aparecerem no output
  • mas se o stream errar, vamos então subscrever novamente o input Observável, e criar um novo stream

Quando voltar a tentar?

A grande questão aqui é, quando é que vamos voltar a subscrever o input Observável, e tentar novamente executar o fluxo de entrada?

  • vamos tentar isso imediatamente?
  • vamos esperar por um pequeno atraso, esperando que o problema seja resolvido e depois tentar novamente?
  • vamos tentar novamente apenas uma quantidade limitada de vezes, e depois errar o fluxo de saída?

Para responder a estas questões, vamos precisar de um segundo Observável auxiliar, que vamos chamar de Observável Notificador. É o Notificador
Observável que vai determinar quando ocorre a tentativa de reentrada.

O Notificador Observável vai ser usado pelo operador de reentrada, que é o coração da estratégia de reentrada.

RxJs retryWhen Operator Marble Diagram

Para entender como funciona o retryWhen Observable, vamos dar uma olhada no seu diagrama de mármore:

RxJs retryWhen Operator

Notem que o Observable que está sendo re-testado é o 1-2 Observable na segunda linha do topo, e não o Observable na primeira linha.

O Observável na primeira linha com valores r-r é a Notificação Observável, que vai determinar quando uma tentativa de re-tentativa deve ocorrer.

Quebrando como funciona a re-tentativa

Dividamos o que está acontecendo neste diagrama:

  • O Observável 1-2 é subscrito, e seus valores são refletidos imediatamente na saída do Observável retornado por re-tenta Quando
  • mesmo depois que o Observável 1-2 é completado, ele ainda pode ser re-testado
  • a notificação Observável então emite um valor r, muito depois do Observável 1-2 ter completado
  • O valor emitido pela notificação Observável (neste caso r) pode ser qualquer coisa
  • o que importa é o momento em que o valor r foi emitido, porque é isso que vai desencadear o 1-2 Observável a ser testado de novo
  • O Observável 1-2 é subscrito de novo por retryWhen, e os seus valores são novamente reflectidos no output Observável de retryWhen
  • A notificação Observável vai então emitir novamente outro r valor, e a mesma coisa acontece: os valores de um fluxo 1-2 recentemente subscrito vão começar a ser refletidos na saída de retry Quando
  • mas então, a notificação Observável eventualmente completa
  • nesse momento, a tentativa de nova tentativa em curso do 1-2 Observável também é concluída mais cedo, o que significa que apenas o valor 1 foi emitido, mas não 2

Como podemos ver, nova tentativa Quando simplesmente se tenta novamente o input Observável cada vez que a Notificação Observável emite um valor!

Agora compreendemos como funciona a Notification Observable Quando podemos ver, vamos ver como podemos criar uma Notification Observable.

Criando uma Notification Observable

Temos de criar a Notification Observable directamente na função passada para o operador retryWhen. Esta função toma como argumento de entrada um Erros Observáveis, que emite como valores os erros do input Observável.

Então, ao subscrever este Erros Observáveis, sabemos exatamente quando um erro ocorre. Vejamos agora como podemos implementar uma estratégia de nova tentativa imediata usando os Erros Observáveis.

Immediate Retry Strategy

A fim de tentar novamente o observável falhado imediatamente após a ocorrência do erro, tudo o que temos de fazer é retornar os Erros Observáveis sem qualquer outra alteração.

Neste caso, estamos apenas a pipar o operador da torneira para fins de registo, pelo que os Erros Observáveis permanecem inalterados:

Lembramos que o Observável que estamos a retornar da nova tentativa Quando a chamada de função é a Notificação Observável!

O valor que ela emite não é importante, só é importante quando o valor é emitido porque é isso que vai desencadear uma tentativa de reentrada.

Immediate Retry Console Output

Se agora executarmos este programa, vamos encontrar a seguinte saída no console:

retry Quando a saída do console

Como podemos ver, a requisição HTTP falhou inicialmente, mas depois foi tentada uma re tentativa e a segunda vez que a requisição passou com sucesso.

Vejamos agora o atraso entre as duas tentativas, inspecionando o log de rede:

RxJs retryQuando o log de rede

Como podemos ver, a segunda tentativa foi emitida imediatamente após a ocorrência do erro, como esperado.

Estratégia de Retry Atrasado

Passemos agora implementar uma estratégia alternativa de recuperação de erro, onde esperamos por exemplo por 2 segundos após a ocorrência do erro, antes de tentar novamente.

Esta estratégia é útil para tentar recuperar de certos erros, como por exemplo, pedidos de rede falhados causados pelo alto tráfego do servidor.

Nos casos onde o erro é intermitente, podemos simplesmente tentar novamente o mesmo pedido após um pequeno atraso, e o pedido pode passar pela segunda vez sem qualquer problema.

A função de criação do temporizador Observável

Para implementar a Estratégia de Retry Atrasado, precisaremos criar uma Notification Observable cujos valores são emitidos dois segundos após cada ocorrência de erro.

Tentemos então criar uma Notification Observable usando a função de criação do temporizador. Esta função do temporizador vai pegar alguns argumentos:

  • um atraso inicial, antes do qual não serão emitidos valores
  • um intervalo periódico, caso queiramos emitir novos valores periodicamente

Vejamos então o diagrama de mármore para a função do temporizador:

O operador do temporizador

Como podemos ver, o primeiro valor 0 será emitido apenas após 3 segundos, e então temos um novo valor a cada segundo.

Note que o segundo argumento é opcional, o que significa que se o deixarmos de fora o nosso Observável emitirá apenas um valor (0) após 3 segundos e depois completado.

Este Observável parece ser um bom começo por ser capaz de atrasar as nossas tentativas de reentrada, então vamos ver como podemos combiná-lo com a reentrada Quando e atraso Quando operadores.

O atraso Quando operador

Uma coisa importante a ter em mente sobre a reentrada Quando operador, é que a função que define a Notification Observable é chamada apenas uma vez.

Então só temos uma hipótese de definir a nossa Notification Observable, que sinaliza quando as tentativas de reentrada devem ser feitas.

Vamos definir a Notification Observable pegando nos Erros Observáveis e aplicando-lhe o atrasoWhen Operator.

Imagine que neste diagrama de mármore, a fonte Observável a-b-c é o Erros Observáveis, que emite erros HTTP falhados ao longo do tempo:

O operador do temporizador

AtrasoQuando o operador avaria

Vamos seguir o diagrama, e aprender como funciona o atrasoQuando o operador:

  • Cada valor na entrada Erros Observáveis vai ser atrasado antes de aparecer na saída Observável
  • o atraso por cada valor pode ser diferente, e vai ser criado de uma forma completamente flexível
  • para determinar o atraso, vamos chamar a função passada para atraso Quando (chamada função seletora de duração) por cada valor do input Erros Observáveis
  • essa função vai emitir um Observável que vai determinar quando o atraso de cada valor de input tiver decorrido
  • cada um dos valores a-b-c tem seu próprio seletor de duração Observável, que eventualmente emitirá um valor (que pode ser qualquer coisa) e depois completar
  • quando cada um destes seleccionadores de duração Observables emite valores, então o valor de entrada correspondente a-b-c vai aparecer na saída de atrasoQuando
  • nota que o valor b aparece na saída após o valor c, isto é normal
  • isto é porque o seletor de duração Observável de b (a terceira linha horizontal do topo) só emitiu seu valor após o seletor de duração Observável de c, e isso explica porque c aparece na saída antes de b

Atraso na implementação da Estratégia de Retry

Vamos agora juntar tudo isso e ver como podemos tentar de novo consecutivamente um pedido HTTP falho 2 segundos após cada erro ocorrer:

Vamos decompor o que está a acontecer aqui:

  • lembramos que a função passou para nova tentativa Quando vai ser chamada apenas uma vez
  • estamos retornando naquela função um Observável que emitirá valores sempre que uma nova tentativa for necessária
  • cada vez que houver um erro, o atraso Quando o operador vai criar um seletor de duração Observable, chamando a função timer
  • este seletor de duração Observable vai emitir o valor 0 após 2 segundos, e então completar
  • a partir do momento em que isso acontecer, o atrasoQuando o Observable sabe que o atraso de um dado erro de entrada já passou
  • somente uma vez que esse atraso só passou (2 segundos após a ocorrência do erro), o erro aparece na saída da notificação Observable
  • se um valor for emitido na notificação Observable, a reentrada Quando o operador vai então e só então executar uma tentativa de reentrada

Estratégia de saída do Console de Estratégia de Retrização

Vejamos agora como isto se parece no console! Aqui está um exemplo de um pedido HTTP que foi testado 5 vezes, já que as 4 primeiras vezes estavam em erro:

O operador de temporizador

E aqui está o log de rede para a mesma sequência de tentativas:

O operador de temporizador

Como podemos ver, as tentativas de reentrada só aconteceram 2 segundos após a ocorrência do erro, como esperado!

E com isto, completamos a nossa visita guiada a algumas das estratégias de manipulação de erros RxJs mais utilizadas disponíveis, vamos agora embrulhar as coisas e fornecer algum código de amostra em execução.

Running Github repository (with code samples)

Para tentar estas múltiplas estratégias de tratamento de erros, é importante ter um playground funcional onde você pode tentar lidar com pedidos HTTP falhos.

Este playground contém uma pequena aplicação em execução com um backend que pode ser usado para simular erros HTTP tanto aleatória quanto sistematicamente. Aqui está o aspecto da aplicação:

RxJs sample application

Conclusions

Como já vimos, compreender o tratamento de erros RxJs tem tudo a ver com compreender primeiro os fundamentos do contrato Observável.

Temos de ter em mente que qualquer fluxo só pode errar uma vez, e isso é exclusivo com a conclusão do fluxo; apenas uma das duas coisas pode acontecer.

A fim de recuperar de um erro, a única maneira é gerar um stream de substituição como alternativa ao stream de saída errado, como acontece no caso do catchError ou retryWhen Operators.

Eu espero que você tenha gostado deste post, se você gostaria de aprender muito mais sobre RxJs, recomendamos verificar o curso RxJs In Practice Course, onde muitos padrões e operadores úteis são cobertos com muito mais detalhes.

Deixe uma resposta

O seu endereço de email não será publicado.