RxJs felhantering:

Felhantering är en viktig del av RxJs, eftersom vi kommer att behöva den i nästan alla reaktiva program som vi skriver.

Felhantering i RxJS är troligen inte lika välförstått som andra delar av biblioteket, men det är faktiskt ganska enkelt att förstå om vi först fokuserar på att förstå Observable-kontraktet i allmänhet.

I det här inlägget kommer vi att ge en komplett guide som innehåller de vanligaste felhanteringsstrategierna som du kommer att behöva för att täcka de flesta praktiska scenarier, med början i grunderna (Observable-kontraktet).

Innehållsförteckning

I det här inlägget kommer vi att täcka följande ämnen:

  • The Observable contract and Error Handling
  • RxJs subscribe and error callbacks
  • The catchError Operator
  • The Catch and Replace Strategy
  • throwError och Catch and Rethrow Strategy
  • Användning av catchError flera gånger i en Observable-kedja
  • The finalize Operator
  • The Retry Strategi
  • Därefter retryWhen Operatör
  • Skapa en Observable för anmälan
  • Strategi för omedelbar återkallelse
  • Strategi för försenad återkallelse
  • Den fördröjda återkallelsen
  • Den fördröjda återkallelsen Operator
  • The timer Observable creation function
  • Running Github repository (with code samples)
  • Conclusions

So without further ado, Låt oss komma igång med vår djupdykning i RxJs felhantering!

The Observable Contract and Error Handling

För att förstå felhantering i RxJs måste vi först förstå att en viss ström bara kan ge fel en gång. Detta definieras av kontraktet Observable, som säger att en ström kan sända ut noll eller fler värden.

Kontraktet fungerar på det sättet eftersom det är precis så alla strömmar som vi observerar i vår körtid fungerar i praktiken. Nätverksförfrågningar kan till exempel misslyckas.

En stream kan också slutföra, vilket innebär att:

  • strömmen har avslutat sin livscykel utan något fel
  • efter slutförandet kommer strömmen inte att avge några fler värden

Som ett alternativ till slutförandet kan en stream också göra fel, vilket innebär att:

  • strömmen har avslutat sin livscykel med ett fel
  • efter att felet kastats kommer strömmen inte att avge några andra värden

Observera att fullbordan eller fel utesluter varandra:

  • Om strömmen avslutar kan den inte göra fel efteråt
  • Om strömmen gör fel kan den inte avsluta efteråt

Bemärk också att det inte finns någon skyldighet för strömmen att avsluta eller göra fel, dessa två möjligheter är valfria. Men endast en av dessa två kan inträffa, inte båda.

Detta innebär att när en viss ström har fel kan vi inte använda den längre, i enlighet med Observable-kontraktet. Du tänker säkert just nu, hur kan vi återhämta oss från ett fel då?

RxJs subscribe and error callbacks

För att se RxJs felhanteringsbeteende i praktiken ska vi skapa en stream och prenumerera på den. Låt oss komma ihåg att subscribe-anropet tar tre valfria argument:

  • en success handler-funktion, som anropas varje gång strömmen avger ett värde
  • en error handler-funktion, som anropas endast om ett fel uppstår. Denna handläggare tar emot felet själv
  • en handläggarfunktion för slutförandet, som endast anropas om strömmen avslutas

Exempel på avslutande beteende

Om strömmen inte ger upphov till något fel är det här vad vi skulle se i konsolen:

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

Som vi kan se avger den här HTTP-strömmen endast ett värde, och sedan avslutas den, vilket innebär att inga fel har inträffat.

Men vad händer om strömmen kastar ett fel istället? I så fall kommer vi att se följande i konsolen istället:

RxJs Error Handling console output

Som vi kan se så avgav strömmen inget värde och det uppstod omedelbart ett fel. Efter felet inträffade inget slutförande.

Begränsningar av subscribe-felhanteraren

Hantering av fel med hjälp av subscribe-anropet är ibland allt vi behöver, men detta tillvägagångssätt för felhantering är begränsat. Med detta tillvägagångssätt kan vi till exempel inte återhämta oss från felet eller sända ut ett alternativt fallback-värde som ersätter det värde som vi förväntade oss från backend.

Därefter ska vi lära oss några operatörer som gör det möjligt för oss att implementera några mer avancerade felhanteringsstrategier.

Operatorn catchError

I synkron programmering har vi möjlighet att linda in ett kodblock i en try-klausul, fånga upp ett eventuellt fel som det kan kasta med ett catch-block och sedan hantera felet.

Här ser syntaxen för synkron catch ut:

Denna mekanism är mycket kraftfull eftersom vi på ett och samma ställe kan hantera alla fel som inträffar inuti try/catch-blocket.

Problemet är att i Javascript är många operationer asynkrona, och ett HTTP-anrop är ett sådant exempel där saker och ting sker asynkront.

RxJs ger oss något som ligger nära denna funktionalitet, via RxJs catchError Operator.

Hur fungerar catchError?

Som vanligt och som med alla RxJs Operator är catchError helt enkelt en funktion som tar in en Observable input och ger ut en Observable output.

Med varje anrop till catchError måste vi skicka över en funktion som vi kallar felhanteringsfunktionen.

Operatorn catchError tar som ingång en Observable som kan ge upphov till fel, och börjar avge värdena i ingångsobservable i sin utgångsobservable.

Om inget fel inträffar fungerar den utgångsobservable som produceras av catchError på exakt samma sätt som ingångsobservable.

Hur sker det om ett fel uppstår?

Om ett fel inträffar kommer dock logiken för catchError att träda in. Operatören catchError kommer att ta felet och skicka det till felhanteringsfunktionen.

Denna funktion förväntas returnera en Observable som kommer att vara en ersättningsobservable för strömmen som just fick ett fel.

Vi ska komma ihåg att ingångsströmmen till catchError har gått ut, så enligt Observable-kontraktet kan vi inte använda den längre.

Denna ersättnings-Observable ska sedan prenumereras på och dess värden ska användas i stället för den felande ingångs-Observable.

The Catch and Replace Strategy

Låt oss ge ett exempel på hur catchError kan användas för att tillhandahålla en ersättnings-Observable som avger reservvärden:

Låt oss bryta ner implementeringen av catch and replace-strategin:

  • Vi överlämnar en funktion till operatören catchError, som är felhanteringsfunktionen
  • Felhanteringsfunktionen anropas inte omedelbart, och i allmänhet anropas den vanligtvis inte
  • endast när ett fel inträffar i ingångsobservabeln catchError, kommer felhanteringsfunktionen att anropas
  • om ett fel inträffar i ingångsströmmen, returnerar denna funktion då en Observable som byggs med hjälp av of()-funktionen
  • of()-funktionen bygger en Observable som endast avger ett värde () och sedan avslutas den
  • -funktionen för felhantering returnerar återställningsobservabeln (of()), som blir prenumererad av operatorn catchError
  • Värdena i recovery Observable sänds sedan ut som ersättningsvärden i output Observable som returneras av catchError

Som slutresultat kommer http$ Observable inte att göra något fel längre! Här är resultatet som vi får i konsolen:

HTTP response HTTP request completed.

Som vi kan se anropas inte felhanterings-callbacken i subscribe() längre. Istället händer följande:

  • det tomma arrayvärdet sänds ut
  • observabeln http$ avslutas sedan

Som vi kan se användes ersättningsobservabeln för att tillhandahålla ett standardvärde för återkoppling () till prenumeranterna av http$, trots att den ursprungliga observabeln hade fel.

Observera att vi också kunde ha lagt till lite lokal felhantering innan vi returnerade ersättningsobservabeln!

Och detta täcker Catch and Replace-strategin, låt oss nu se hur vi också kan använda catchError för att rethrow felet, istället för att tillhandahålla fallback-värden.

The Catch and Rethrow Strategy

Låt oss börja med att lägga märke till att ersättningsobservabeln som tillhandahålls via catchError i sig själv också kan ge ett fel, precis som vilken annan observabel som helst.

Och om det händer kommer felet att spridas till prenumeranterna på utgången Observable av catchError.

Detta felspridningsbeteende ger oss en mekanism för att återkasta felet som fångats upp av catchError, efter att ha hanterat felet lokalt. Vi kan göra det på följande sätt:

Uppdelning av Catch and Rethrow

Låt oss steg för steg dela upp implementeringen av Catch and Rethrow-strategin:

  • precis som tidigare fångar vi felet och returnerar en ersättningsobservabel
  • men den här gången, istället för att tillhandahålla ett ersättningsutgångsvärde som , hanterar vi nu felet lokalt i catchError-funktionen
  • i det här fallet loggar vi helt enkelt felet till konsolen, men vi kan istället lägga till vilken lokal felhanteringslogik som helst, som till exempel att visa ett felmeddelande till användaren
  • Vi returnerar sedan en ersättningsobservabel som den här gången skapades med hjälp av throwError
  • throwError skapar en observabel som aldrig avger något värde. Istället felar den omedelbart med samma fel som fångades upp av catchError
  • Detta innebär att utgångsobservabeln för catchError också felar med exakt samma fel som kastades av ingången till catchError
  • Detta innebär att vi framgångsrikt har lyckats återkasta felet som ursprungligen kastades av ingångsobservabeln för catchError till dess utgångsobservabeln
  • Felet kan nu hanteras vidare av resten av observableringskedjan, om det behövs

Om vi nu kör koden ovan är här resultatet som vi får i konsolen:

RxJs Error Handling console output

Som vi kan se loggades samma fel både i catchError-blocket och i funktionen för hantering av prenumerationsfel, som förväntat.

Användning av catchError flera gånger i en Observable-kedja

Bemärk att vi kan använda catchError flera gånger vid olika punkter i Observable-kedjan om det behövs, och anta olika felstrategier vid varje punkt i kedjan.

Vi kan till exempel fånga upp ett fel högre upp i Observable-kedjan, hantera det lokalt och återkasta det, och sedan längre ner i Observable-kedjan kan vi fånga upp samma fel igen och den här gången ge ett reservvärde (istället för att återkasta):

Om vi kör koden ovan är det här resultatet vi får i konsolen:

RxJs Map Operator marmordiagram

Som vi kan se återkastades felet faktiskt initialt, men det nådde aldrig funktionen för att hantera felet. Istället sändes fallback -värdet ut, som förväntat.

The Finalize Operator

Förutom ett catch-block för hantering av fel erbjuder den synkrona Javascript-syntaxen också ett finally-block som kan användas för att köra kod som vi alltid vill ha exekverad.

Det finally-blocket används typiskt sett för att släppa dyra resurser, som till exempel att stänga nätverksanslutningar eller släppa minne.

Till skillnad från koden i catch-blocket kommer koden i finally-blocket att exekveras oberoende av om ett fel kastas eller inte:

RxJs ger oss en operatör som har ett liknande beteende som finally-funktionaliteten, kallad finalize Operator.

Notera: vi kan inte kalla det för finally-operatorn istället, eftersom finally är ett reserverat nyckelord i Javascript

Finalize-operatorn exempel

Samma som catchError-operatorn kan vi lägga till flera finalize-anrop på olika ställen i Observable-kedjan om det behövs, för att se till att de flera resurserna släpps korrekt:

Låt oss nu köra den här koden och se hur de flera finalize-blocken exekveras:

RxJs Error Handling konsolutgång

Bemärk att det sista finalize-blocket exekveras efter funktionerna subscribe value handler och completion handler.

The Retry Strategy

Som ett alternativ till att kasta felet på nytt eller tillhandahålla reservvärden kan vi också helt enkelt försöka igen för att prenumerera på den felande Observable.

Låt oss komma ihåg att när strömmen väl har fått ett fel kan vi inte återskapa den, men inget hindrar oss från att återigen prenumerera på den Observable som strömmen härstammar från, och skapa en ny ström.

Här är hur detta fungerar:

  • Vi kommer att ta inmatningsobservationen och prenumerera på den, vilket skapar en ny ström
  • Om den strömmen inte ger upphov till fel, kommer vi att låta dess värden dyka upp i utmatningen
  • Men om strömmen ger upphov till fel, kommer vi att prenumerera på nytt på inmatningsobservationen och skapa en helt ny ström

När vi ska försöka igen?

Den stora frågan här är, när ska vi prenumerera igen på input Observable och försöka köra inputströmmen igen?

  • Ska vi försöka igen omedelbart?
  • Ska vi vänta med en liten fördröjning och hoppas att problemet är löst och sedan försöka igen?
  • ska vi försöka igen bara ett begränsat antal gånger och sedan göra fel i utdataströmmen?

För att besvara dessa frågor behöver vi en andra hjälpobservabel, som vi kommer att kalla Notifier Observable. Det är Notifier
Observable som kommer att avgöra när retry-försöket sker.

Notifier Observable kommer att användas av retryWhen Operator, som är hjärtat i Retry Strategy.

RxJs retryWhen Operator Marmordiagram

För att förstå hur retryWhen Observable fungerar tar vi en titt på dess marmordiagram:

RxJs retryWhen Operator

Bemärk att den Observable som prövas på nytt är den 1-2 Observable i den andra raden uppifrån och inte den Observable som står i den första raden.

Observabeln på första raden med värdena r-r är Notification Observable, som kommer att avgöra när ett försök till omprövning ska ske.

Brytning av hur retryWhen fungerar

Låt oss bryta ner vad som händer i det här diagrammet:

  • Observerbarheten 1-2 prenumereras och dess värden återspeglas omedelbart i den utgående observerbarheten som returneras av retryNär
  • den även efter att observerbarheten 1-2 har slutförts kan den fortfarande prövas på nytt
  • närvaron Observable sänder då ut ett värde r, långt efter att Observable 1-2 har avslutats
  • Värdet som sänds ut av notification Observable (i det här fallet r) kan vara vad som helst
  • Det viktiga är när värdet r sänds ut, eftersom det är det som kommer att utlösa att 1-2 Observable ska prövas på nytt
  • Observable 1-2 prenumereras igen av retryWhen, och dess värden återspeglas återigen i retryWhen:s utgångsobservable
  • Notifikationsobservable kommer sedan att sända ut ett annat r-värde igen, och samma sak inträffar: Värdena i en nytecknad 1-2 stream kommer att börja reflekteras i retryWhen
  • utgången av retryWhen
  • men sedan avslutas notification Observable så småningom
  • i det ögonblicket, det pågående försöket att göra ett nytt försök för 1-2 Observable avslutas också tidigt, vilket innebär att endast värdet 1 sändes ut, men inte 2

Som vi kan se, gör retryWhen helt enkelt ett nytt försök för input Observable varje gång som Notification Observable sänder ut ett värde!

När vi nu förstår hur retryWhen fungerar, ska vi se hur vi kan skapa en Notification Observable.

Skapa en Notification Observable

Vi måste skapa Notification Observable direkt i den funktion som överlämnats till retryWhen-operatören. Denna funktion tar som ingångsargument en Errors Observable, som som avger som värden felen i den ingående Observable.

Så genom att prenumerera på denna Errors Observable vet vi exakt när ett fel inträffar. Låt oss nu se hur vi skulle kunna implementera en strategi för omedelbar återgång med hjälp av Errors Observable.

Immediate Retry Strategy

För att återgå till den misslyckade observabeln omedelbart efter att felet inträffat, är allt vi behöver göra att returnera Errors Observable utan några ytterligare ändringar.

I det här fallet rör vi bara tappoperatorn för loggningsändamål, så Errors Observable förblir oförändrad:

Vi ska komma ihåg att den Observable som vi returnerar från retryWhen-funktionsanropet är Notification Observable!

Värdet som sänds ut är inte viktigt, det är bara viktigt när värdet sänds ut eftersom det är det som kommer att utlösa ett försök till omprövning.

Omedelbart Retry Konsolutgång

Om vi nu kör det här programmet kommer vi att hitta följande utdata i konsolen:

retryWhen konsolutgång

Som vi kan se misslyckades HTTP-begäran först, men sedan gjordes ett försök att göra ett nytt försök och den andra gången gick begäran igenom med framgång.

Vi kan nu ta en titt på fördröjningen mellan de två försöken genom att inspektera nätverksloggen:

RxJs retryWhen network log

Som vi kan se utfärdades det andra försöket omedelbart efter att felet inträffat, som förväntat.

Strategi för försenat återförsök

Vi ska nu implementera en alternativ strategi för felåterställning, där vi till exempel väntar 2 sekunder efter att felet inträffat innan vi försöker igen.

Denna strategi är användbar för att försöka återhämta sig från vissa fel som till exempel misslyckade nätverksförfrågningar som orsakas av hög servertrafik.

I de fall då felet är intermittent kan vi helt enkelt göra ett nytt försök med samma begäran efter en kort fördröjning, och begäran kanske går igenom andra gången utan problem.

Funktionen för skapande av timer Observable

För att implementera den fördröjda retry-strategin måste vi skapa en Notification Observable vars värden sänds ut två sekunder efter varje feltillfälle.

Låt oss då försöka skapa en Notification Observable genom att använda funktionen för skapande av timer. Denna timerfunktion kommer att ta emot ett par argument:

  • en initial fördröjning, före vilken inga värden kommer att sändas ut
  • ett periodiskt intervall, om vi vill sända ut nya värden med jämna mellanrum

Låt oss sedan ta en titt på marmordiagrammet för timerfunktionen:

The timer Operator

Som vi kan se kommer det första värdet 0 att sändas ut först efter 3 sekunder, och sedan har vi ett nytt värde varje sekund.

Bemärk att det andra argumentet är valfritt, vilket innebär att om vi utelämnar det kommer vår Observable endast att sända ut ett värde (0) efter 3 sekunder och sedan avslutas.

Denna Observable ser ut att vara en bra start för att kunna fördröja våra försök till omprövning, så låt oss se hur vi kan kombinera den med operatörerna retryWhen och delayWhen.

Operatorn delayWhen

En viktig sak att tänka på när det gäller operatorn retryWhen är att den funktion som definierar Notification Observable endast anropas en gång.

Så vi får bara en chans att definiera vår Notification Observable, som signalerar när försök till omprövning ska göras.

Vi kommer att definiera Notification Observable genom att ta Errors Observable och applicera den på delayWhen Operator.

Föreställ dig att i det här marmordiagrammet är källan Observable a-b-c Errors Observable, som avger misslyckade HTTP-fel över tiden:

The timer Operator

delayWhen Operator breakdown

Låt oss följa diagrammet, och lära oss hur delayWhen Operator fungerar:

  • Varje värde i inmatningen Errors Observable kommer att fördröjas innan det visas i utmatningen Observable
  • fördröjningen för varje värde kan vara olika, och kommer att skapas på ett helt flexibelt sätt
  • för att bestämma fördröjningen, kommer vi att anropa den funktion som överlämnats till delayWhen (kallad duration selector-funktionen) för varje värde i ingångsvärdena Errors Observable
  • Denna funktion kommer att avge en Observable som kommer att bestämma när fördröjningen för varje ingångsvärde har förflutit
  • Varje värde a-b-c har sin egen duration selector Observable, som så småningom kommer att sända ut ett värde (som kan vara vad som helst) och sedan avslutas
  • när var och en av dessa varaktighetsväljaren Observables sänder ut värden, så kommer motsvarande ingångsvärde a-b-c att dyka upp i utgången av delayWhen
  • Observera att värdet b dyker upp i utgången efter värdet c, detta är normalt
  • Det beror på att duration selector Observable b (den tredje horisontella linjen uppifrån) endast avgav sitt värde efter duration selector Observable c, och det förklarar varför c dyker upp i utmatningen före b

Delayed Retry Strategy implementation

Vi ska nu sätta ihop allt detta och se hur vi kan göra ett nytt försök i följd av en misslyckad HTTP-förfrågan 2 sekunder efter att varje fel inträffat:

Vi ska nu dela upp vad som händer här:

  • Vi måste komma ihåg att den funktion som överlämnats till retryWhen endast kommer att anropas en gång
  • Vi returnerar i den funktionen en Observable som kommer att skicka ut värden varje gång ett nytt försök behövs
  • Varje gång det uppstår ett fel, Operatören delayWhen kommer att skapa en observationsobjekt för varaktighet genom att anropa timer-funktionen
  • Denna observationsobjekt för varaktighet kommer att avge värdet 0 efter 2 sekunder och sedan slutföra
  • när detta inträffar, fördröjningenNär Observable vet att fördröjningen för ett givet inmatningsfel har löpt ut
  • när denna fördröjning löper ut (2 sekunder efter det att felet inträffade), visas felet i utmatningen av anmälan Observable
  • när ett värde sänds ut i anmälan Observable, kommer operatören retryWhen då och endast då att utföra ett nytt försök

Retry Strategy Console Output

Vi ska nu se hur detta ser ut i konsolen! Här är ett exempel på en HTTP-förfrågan som försökte om 5 gånger, eftersom de första 4 gångerna var fel:

The timer Operator

Och här är nätverksloggen för samma omförsökssekvens:

The timer Operator

Som vi kan se skedde omförsöken bara 2 sekunder efter att felet inträffade, som förväntat!

Och med detta har vi avslutat vår guidade tur av några av de mest använda RxJs felhanteringsstrategierna som finns tillgängliga, låt oss nu avsluta det hela och ge lite löpande exempelkod.

Running Github repository (with code samples)

För att kunna prova dessa olika felhanteringsstrategier är det viktigt att ha en fungerande lekplats där du kan prova att hantera misslyckade HTTP-begäranden.

Denna lekplats innehåller en liten körbar applikation med en backend som kan användas för att simulera HTTP-fel antingen slumpmässigt eller systematiskt. Så här ser programmet ut:

RxJs sample application

Conclusions

Som vi har sett handlar förståelsen av RxJs felhantering först och främst om att förstå grunderna i Observable-kontraktet.

Vi måste komma ihåg att en given stream bara kan ge fel en gång, och det är exklusivt med stream completion; bara en av de två sakerna kan hända.

För att återhämta sig från ett fel är det enda sättet att på något sätt generera ett ersättningsflöde som ett alternativ till det felade flödet, som det sker i fallet med operatörerna catchError eller retryWhen.

Jag hoppas att du har gillat det här inlägget, om du vill lära dig mycket mer om RxJs rekommenderar vi att du kollar in kursen RxJs In Practice Course, där massor av användbara mönster och operatörer täcks i mycket mer detalj.

Lämna ett svar

Din e-postadress kommer inte publiceras.