RxJs fejlhåndtering:

Fejlhåndtering er en vigtig del af RxJs, da vi får brug for den i stort set alle reaktive programmer, som vi skriver.

Fejlhåndtering i RxJS er sandsynligvis ikke så velforstået som andre dele af biblioteket, men det er faktisk ret nemt at forstå, hvis vi først fokuserer på at forstå Observable-kontrakten i almindelighed.

I dette indlæg vil vi give en komplet vejledning, der indeholder de mest almindelige fejlbehandlingsstrategier, som du har brug for for at dække de fleste praktiske scenarier, startende med det grundlæggende (Observable-kontrakten).

Indholdsfortegnelse

I dette indlæg vil vi dække følgende emner:

  • The Observable-kontrakten og fejlhåndtering
  • RxJs subscribe and error callbacks
  • The catchError Operator
  • The Catch and Replace Strategy
  • throwError og Catch and Rethrow-strategien
  • Brug af catchError flere gange i en Observable-kæde
  • The finalize Operator
  • The Retry Strategy
  • Den retryWhen Operator
  • Skabelse af en notifikationsobservabel
  • Immediate Retry Strategy
  • Delayed Retry Strategy
  • The delayWhen Operator
  • The timer Observable creation function
  • Running Github repository (with code samples)
  • Conclusions

So without further ado, lad os komme i gang med vores RxJs Error Handling deep dive!

Den observerbare kontrakt og fejlhåndtering

For at forstå fejlhåndtering i RxJs skal vi først forstå, at enhver given stream kun kan fejle én gang. Dette er defineret af Observable-kontrakten, som siger, at en stream kan udsende nul eller flere værdier.

Kontrakten fungerer på den måde, fordi det netop er sådan, at alle de streams, som vi observerer i vores runtime, fungerer i praksis. Netværksanmodninger kan f.eks. mislykkes.

En stream kan også afsluttes, hvilket betyder, at:

  • strømen har afsluttet sin livscyklus uden fejl
  • efter afslutning vil strømmen ikke udsende yderligere værdier

Som et alternativ til afslutning kan en stream også fejle, hvilket betyder, at:

  • strømen har afsluttet sin livscyklus med en fejl
  • efter at fejlen er kastet, vil strømmen ikke udsende andre værdier

Bemærk, at færdiggørelse eller fejl udelukker hinanden gensidigt:

  • hvis strømmen afsluttes, kan den ikke fejle bagefter
  • hvis strømmen fejler, kan den ikke afsluttes bagefter

Bemærk også, at der ikke er nogen forpligtelse for strømmen til at afslutte eller fejle, disse to muligheder er valgfrie. Men kun en af de to kan forekomme, ikke begge.

Det betyder, at når en bestemt stream fejler, kan vi ikke længere bruge den, i henhold til Observable-kontrakten. Du tænker sikkert på dette tidspunkt, hvordan kan vi så genoprette en fejl?

RxJs subscribe and error callbacks

For at se RxJs’ fejlhåndteringsadfærd i praksis skal vi oprette en stream og abonnere på den. Lad os huske, at subscribe-kaldet tager tre valgfrie argumenter:

  • en succeshåndteringsfunktion, som kaldes hver gang strømmen afgiver en værdi
  • en fejlhåndteringsfunktion, som kun bliver kaldt, hvis der opstår en fejl. Denne handler modtager selv fejlen
  • en completion-handlerfunktion, der kun bliver kaldt, hvis strømmen afsluttes

Completion Behavior Example

Hvis strømmen ikke giver fejl, så er det, hvad vi ville se i konsollen:

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

Som vi kan se, udsender denne HTTP-stream kun én værdi, og derefter afsluttes den, hvilket betyder, at der ikke er opstået nogen fejl.

Men hvad sker der, hvis strømmen i stedet kaster en fejl? I så fald vil vi se følgende i konsollen i stedet:

RxJs Error Handling console output

Som vi kan se, udsendte strømmen ingen værdi, og der blev straks begået en fejl. Efter fejlen skete der ingen færdiggørelse.

Begrænsninger ved subscribe-fejlhåndtering

Håndtering af fejl ved hjælp af subscribe-opkaldet er nogle gange alt, hvad vi har brug for, men denne fejlhåndteringsmetode er begrænset. Ved hjælp af denne fremgangsmåde kan vi f.eks. ikke genoprette fejlen eller udsende en alternativ fallback-værdi, der erstatter den værdi, som vi forventede fra backenden.

Lad os derefter lære nogle få operatører, der giver os mulighed for at implementere nogle mere avancerede fejlbehandlingsstrategier.

The catchError Operator

I synkron programmering har vi mulighed for at pakke en kodeblok ind i en try-klausul, fange en eventuel fejl, som den måtte kaste, med en catch-blok og derefter håndtere fejlen.

Her er, hvordan den synkrone catch-syntaks ser ud:

Denne mekanisme er meget kraftfuld, fordi vi kan håndtere enhver fejl, der sker inden for try/catch-blokken, ét sted.

Problemet er, at i Javascript er mange operationer asynkrone, og et HTTP-opkald er et sådant eksempel, hvor tingene sker asynkront.

RxJs giver os noget, der ligger tæt på denne funktionalitet, via RxJs catchError Operator.

Hvordan fungerer catchError?

Som sædvanlig og som med enhver RxJs Operator er catchError simpelthen en funktion, der tager imod en Observable som input og udsender en Observable som output.

Med hvert kald til catchError skal vi give den en funktion, som vi kalder fejlbehandlingsfunktionen.

CatchError-operatoren tager som input en Observable, der kan give fejl, og begynder at udsende værdierne i input-Observable i sin output-Observable.

Hvis der ikke opstår nogen fejl, fungerer den output-Observable, der produceres af catchError, på nøjagtig samme måde som input-Observable.

Hvad sker der, når der kastes en fejl?

Hvis der derimod opstår en fejl, så træder catchError-logikken i kraft. CatchError-operatoren vil tage fejlen og videregive den til fejlbehandlingsfunktionen.

Denne funktion forventes at returnere en Observable, som vil være en erstatningsobservable for den stream, der lige er gået i fejl.

Lad os huske, at catchError’s inputstream er gået i fejl, så i henhold til Observable-kontrakten kan vi ikke længere bruge den.

Denne erstatnings-Observable vil derefter blive abonneret på, og dens værdier vil blive brugt i stedet for den fejlbehæftede input-Observable.

Fang- og erstatningsstrategien

Lad os give et eksempel på, hvordan catchError kan bruges til at tilvejebringe en erstatnings-Observable, der udsender fallback-værdier:

Lad os bryde implementeringen af catch- og erstatningsstrategien ned:

  • vi videregiver en funktion til catchError-operatoren, som er fejlbehandlingsfunktionen
  • fejlbehandlingsfunktionen kaldes ikke straks, og generelt kaldes den normalt ikke
  • kun når der opstår en fejl i catchError-operatørens indgangs-Oservable, vil fejlbehandlingsfunktionen blive kaldt
  • hvis der opstår en fejl i indgangsstrømmen, returnerer denne funktion så en Observable, der er opbygget ved hjælp af of()-funktionen
  • Den of()-funktion opbygger en Observable, der kun udsender én værdi (), og derefter afsluttes den
  • Fejlehåndteringsfunktionen returnerer den genoprettede Observable (of()), som operatøren catchError
  • underskrives af operatøren catchError

Som slutresultat vil http$ Observable’en ikke længere fejle! Her er det resultat, som vi får i konsollen:

HTTP response HTTP request completed.

Som vi kan se, bliver fejlbehandlings-callbacken i subscribe() ikke længere påberåbt. I stedet sker der følgende:

  • Den tomme arrayværdi udsendes
  • Observablen http$ afsluttes derefter

Som vi kan se, blev erstatningsobservablen brugt til at levere en standard fallback-værdi () til abonnenterne i http$, på trods af at den oprindelige observabel fejlede.

Bemærk, at vi også kunne have tilføjet noget lokal fejlhåndtering, før vi returnerede erstatningsobservablen!

Og dette dækker Catch and Replace-strategien, lad os nu se, hvordan vi også kan bruge catchError til at rethrow fejlen, i stedet for at levere fallback-værdier.

The Catch and Rethrow Strategy

Lad os starte med at bemærke, at den erstatnings-Observable, der leveres via catchError, også selv kan fejle, ligesom enhver anden Observable.

Og hvis det sker, vil fejlen blive propageret til abonnenterne på output Observable af catchError.

Denne fejlpropagationsadfærd giver os en mekanisme til at rethrow den fejl, der er fanget af catchError, efter at fejlen er blevet håndteret lokalt. Det kan vi gøre på følgende måde:

Opdeling af Catch and Rethrow

Lad os trin for trin opdele implementeringen af Catch and Rethrow-strategien:

  • ligesom før fanger vi fejlen og returnerer en erstatningsobservabel
  • men denne gang i stedet for at levere en erstatningsudgangsværdi som , håndterer vi nu fejlen lokalt i catchError-funktionen
  • i dette tilfælde logger vi blot fejlen til konsollen, men vi kunne i stedet tilføje enhver lokal fejlbehandlingslogik, som vi ønsker, som f.eks. at vise en fejlmeddelelse til brugeren
  • Vi returnerer derefter en erstatningsobservabel, som denne gang blev oprettet ved hjælp af throwError
  • throwError opretter en observabel, der aldrig udsender nogen værdi. I stedet giver den straks fejl med den samme fejl, der blev fanget af catchError
  • Det betyder, at output-Observable af catchError også giver fejl med nøjagtig den samme fejl, der blev kastet af input af catchError
  • Det betyder, at det er lykkedes os at genkaste den fejl, der oprindeligt blev kastet af input-Observable af catchError, til dens output-Observable
  • Fejlen kan nu håndteres yderligere af resten af Observable-kæden, hvis det er nødvendigt

Hvis vi nu kører ovenstående kode, er her det resultat, som vi får i konsollen:

RxJs Error Handling console output

Som vi kan se, blev den samme fejl logget både i catchError-blokken og i funktionen til håndtering af abonnementsfejl, som forventet.

Brug af catchError flere gange i en Observable-kæde

Bemærk, at vi kan bruge catchError flere gange på forskellige punkter i Observable-kæden, hvis det er nødvendigt, og vedtage forskellige fejlstrategier på hvert punkt i kæden.

Vi kan f.eks. fange en fejl oppe i Observable-kæden, håndtere den lokalt og genkaste den, og så længere nede i Observable-kæden kan vi fange den samme fejl igen og denne gang give en fallback-værdi (i stedet for at genkaste):

Hvis vi kører ovenstående kode, er dette det output, vi får i konsollen:

RxJs Map Operator marmordiagram

Som vi kan se, blev fejlen faktisk genkastet i første omgang, men den nåede aldrig frem til subscribe-fejlbehandlerfunktionen. I stedet blev fallback -værdien udsendt, som forventet.

Den endelige operatør

Suden en catch-blok til håndtering af fejl giver den synkrone Javascript-syntaks også en finally-blok, der kan bruges til at køre kode, som vi altid ønsker udført.

Den endelige blok bruges typisk til at frigive dyre ressourcer, som f.eks. lukning af netværksforbindelser eller frigivelse af hukommelse.

I modsætning til koden i catch-blokken vil koden i finally-blokken blive udført uafhængigt af, om der bliver kastet en fejl eller ej:

RxJs giver os en operatør, der har en lignende opførsel som finally-funktionaliteten, kaldet finalize-operatoren.

Bemærk: Vi kan ikke kalde det finally-operatoren i stedet, da finally er et reserveret nøgleord i Javascript

Finalize-operatoren Eksempel

Som catchError-operatoren kan vi tilføje flere finalize-opkald forskellige steder i Observable-kæden, hvis det er nødvendigt, for at sikre, at de mange ressourcer frigives korrekt:

Lad os nu køre denne kode og se, hvordan de flere finalize-blokke udføres:

RxJs Error Handling konsoloutput

Bemærk, at den sidste finalize-blok udføres efter subscribe value handler- og completion handler-funktionerne.

Genoptagelsesstrategien

Som et alternativ til at genkaste fejlen eller levere fallback-værdier kan vi også blot forsøge igen at abonnere på den fejlbehæftede Observable.

Lad os huske, at når strømmen fejler, kan vi ikke genoprette den, men intet forhindrer os i at abonnere igen på den Observable, som strømmen blev afledt fra, og oprette en ny strøm.

Her er hvordan dette fungerer:

  • Vi tager input Observable og abonnerer på den, hvilket skaber en ny stream
  • Hvis denne stream ikke fejler, lader vi dens værdier dukke op i output
  • Men hvis streamet fejler, abonnerer vi igen på input Observable og skaber en helt ny stream

Hvornår skal vi prøve igen?

Det store spørgsmål her er, hvornår vi skal abonnere igen på input Observable og forsøge igen at udføre inputstrømmen?

  • skal vi forsøge igen med det samme?
  • skal vi vente med en lille forsinkelse og håbe på, at problemet er løst, og så forsøge igen?
  • skal vi kun forsøge igen et begrænset antal gange og derefter fejlmelde outputstrømmen?

For at kunne besvare disse spørgsmål har vi brug for en anden hjælpeobservabel, som vi kalder Notifier Observable. Det er Notifier
Observable, der skal bestemme, hvornår forsøget på at genoptage forsøget finder sted.

Notifier Observable vil blive brugt af retryWhen Operator, som er hjertet i Retry Strategy.

RxJs retryWhen Operator Marmordiagram

For at forstå, hvordan retryWhen Observable fungerer, skal vi se på dens marmordiagram:

RxJs retryWhen Operator

Bemærk, at den Observable, der forsøges igen, er den 1-2 Observable i anden linje fra toppen og ikke Observable i første linje.

Observablen i den første linje med værdierne r-r er Notification Observable, der skal bestemme, hvornår et forsøg på at gentage forsøget skal finde sted.

Opdeling af, hvordan retryWhen fungerer

Lad os opdele, hvad der foregår i dette diagram:

  • Observatøren 1-2 abonneres på, og dens værdier afspejles straks i outputobservatøren, der returneres af retryWhen
  • selv efter at observatøren 1-2 er afsluttet, kan den stadig forsøges igen
  • notifikationsobservatøren udsender derefter en værdi r, langt efter at Observable 1-2 er afsluttet
  • Den værdi, der udsendes af notifikationsobservablen (i dette tilfælde r), kan være hvad som helst
  • Det vigtige er det øjeblik, hvor værdien r blev udsendt, fordi det er det, der vil udløse, at 1-2 Observable skal genforsøges
  • Observable 1-2 bliver abonneret på igen af retryWhen, og dens værdier afspejles igen i output-Observable af retryWhen
  • Notifikations-Observable vil derefter igen udsende en anden r-værdi, og det samme sker: værdierne i en nyligt abonneret 1-2 stream vil begynde at blive afspejlet i outputtet af retryWhen
  • men derefter afsluttes notification Observable til sidst
  • på det tidspunkt, det igangværende forsøg på gentagelse af 1-2 Observable også afsluttes tidligt, hvilket betyder, at kun værdien 1 blev udsendt, men ikke 2

Som vi kan se, gentager retryWhen simpelthen input Observable, hver gang Notification Observable udsender en værdi!

Nu da vi forstår, hvordan retryWhen fungerer, skal vi se, hvordan vi kan oprette en Notification Observable.

Skabelse af en Notification Observable

Vi skal oprette Notification Observable direkte i den funktion, der overdrages til retryWhen-operatoren. Denne funktion tager som inputargument en Errors Observable, der som værdier udsender fejlene i input Observable.

Så ved at abonnere på denne Errors Observable ved vi præcis, hvornår en fejl opstår. Lad os nu se, hvordan vi kan implementere en strategi for øjeblikkelig gentagelse ved hjælp af Errors Observable.

Immediate Retry Strategy

For at gentage den fejlslagne observable straks efter, at fejlen er opstået, skal vi blot returnere Errors Observable uden yderligere ændringer.

I dette tilfælde er vi blot ved at afpipe tap-operatoren til logningsformål, så Errors Observable forbliver uændret:

Lad os huske, at den Observable, som vi returnerer fra retryWhen-funktionskaldet, er Notification Observable!

Værdien, som den udsender, er ikke vigtig, det er kun vigtigt, når værdien bliver udsendt, fordi det er det, der udløser et forsøg på at genoptage forsøget.

Immediate Retry Console Output

Hvis vi nu udfører dette program, vil vi finde følgende output i konsollen:

retryWhen console output

Som vi kan se, mislykkedes HTTP-anmodningen i første omgang, men derefter blev der forsøgt et nyt forsøg, og anden gang gik anmodningen igennem med succes.

Lad os nu se på forsinkelsen mellem de to forsøg ved at inspicere netværksloggen:

RxJs retryWhen netværkslog

Som vi kan se, blev det andet forsøg udstedt umiddelbart efter, at fejlen opstod, som forventet.

Delayed Retry Strategy

Lad os nu implementere en alternativ fejlgenoprettelsesstrategi, hvor vi f.eks. venter 2 sekunder efter, at fejlen er opstået, før vi forsøger igen.

Denne strategi er nyttig, når vi forsøger at genoprette visse fejl som f.eks. mislykkede netværksanmodninger forårsaget af høj servertrafik.

I de tilfælde, hvor fejlen er intermitterende, kan vi blot forsøge igen med den samme anmodning efter en kort forsinkelse, og anmodningen vil måske gå igennem anden gang uden problemer.

Funktionen til oprettelse af timeren Observable

For at implementere den forsinkede gentagelsesstrategi skal vi oprette en Notification Observable, hvis værdier udsendes to sekunder efter hver fejlforekomst.

Lad os så prøve at oprette en Notification Observable ved hjælp af funktionen til oprettelse af timeren. Denne timerfunktion kommer til at tage et par argumenter:

  • en indledende forsinkelse, før hvilken der ikke udsendes nogen værdier
  • et periodisk interval, hvis vi ønsker at udsende nye værdier med jævne mellemrum

Lad os derefter tage et kig på marmordiagrammet for timerfunktionen:

The timer Operator

Som vi kan se, vil den første værdi 0 først blive udsendt efter 3 sekunder, og derefter har vi en ny værdi hvert sekund.

Bemærk, at det andet argument er valgfrit, hvilket betyder, at hvis vi udelader det, vil vores Observable kun udsende én værdi (0) efter 3 sekunder og derefter afslutte.

Denne Observable ser ud til at være en god start for at kunne forsinke vores forsøg på at gentage forsøgene, så lad os se, hvordan vi kan kombinere den med operatørerne retryWhen og delayWhen.

Den delayWhen-operatør

En vigtig ting at huske på med retryWhen-operatøren er, at den funktion, der definerer Notification Observable, kun kaldes én gang.

Så vi får kun én chance for at definere vores Notification Observable, der signalerer, hvornår genforsøgene skal udføres.

Vi vil definere Notification Observable ved at tage Errors Observable og anvende den delayWhen-operatoren.

Forestil dig, at i dette marmordiagram er kilden Observable a-b-c Errors Observable, der udsender fejlslagne HTTP-fejl over tid:

The timer Operator

delayWhen Operator opdeling

Lad os følge diagrammet og lære, hvordan delayWhen Operator fungerer:

  • Hver værdi i input Errors Observable vil blive forsinket, før den vises i output Observable
  • Den forsinkelse pr. værdi kan være forskellig, og vil blive oprettet på en helt fleksibel måde
  • for at bestemme forsinkelsen, kalder vi den funktion, der er overgivet til delayWhen (kaldet duration selector-funktionen) for hver værdi af input Errors Observable
  • Denne funktion vil afgive en Observable, der vil bestemme, hvornår forsinkelsen for hver inputværdi er udløbet
  • Hver værdi a-b-c har sin egen duration selector Observable, der til sidst vil udsende en værdi (som kan være hvad som helst) og derefter afsluttes
  • når hver af disse varighedsselektorer Observables udsender værdier, så vil den tilsvarende inputværdi a-b-c dukke op i output af delayWhen
  • bemærk, at værdien b dukker op i output efter værdien c, det er normalt
  • det er fordi b duration selector Observable (den tredje vandrette linje fra toppen) først udsendte sin værdi efter duration selector Observable af c, og det forklarer, hvorfor c dukker op i outputtet før b

Delayed Retry Strategy implementation

Lad os nu sætte alt dette sammen og se, hvordan vi kan gentage en fejlslagen HTTP-forespørgsel i træk 2 sekunder efter, at hver fejl er opstået:

Lad os bryde ned, hvad der foregår her:

  • Lad os huske, at den funktion, der er overgivet til retryWhen, kun vil blive kaldt én gang
  • Vi returnerer i den funktion en Observable, der vil udsende værdier, når der er behov for et nyt forsøg
  • hver gang, der opstår en fejl, delayWhen-operatoren vil oprette en varighedsselektor Observable ved at kalde timer-funktionen
  • Denne varighedsselektor Observable vil udsende værdien 0 efter 2 sekunder og derefter afslutte
  • når det sker, delayWhen Observable ved, at forsinkelsen for en given indgangsfejl er udløbet
  • kun når denne forsinkelse er udløbet (2 sekunder efter, at fejlen er opstået), vises fejlen i uddata fra notifikationsobservablen
  • når en værdi bliver udsendt i notifikationsobservablen, vil operatøren retryWhen derefter og kun derefter udføre et forsøg på et nyt forsøg

Retry Strategy Console Output

Lad os nu se, hvordan dette ser ud i konsollen! Her er et eksempel på en HTTP-forespørgsel, der blev genforsøgt 5 gange, da de første 4 gange var fejl:

The timer Operator

Og her er netværksloggen for den samme genforsøgssekvens:

The timer Operator

Som vi kan se, skete genforsøgene kun 2 sekunder efter, at fejlen opstod, som forventet!

Og med dette har vi afsluttet vores guidede tur til nogle af de mest almindeligt anvendte RxJs fejlhåndteringsstrategier, der er tilgængelige, lad os nu pakke tingene ind og give noget kørende prøvekode.

Kørende Github-repository (med kodeeksempler)

For at kunne afprøve disse flere fejlhåndteringsstrategier er det vigtigt at have en fungerende legeplads, hvor du kan prøve at håndtere fejlslagne HTTP-forespørgsler.

Denne legeplads indeholder en lille kørende applikation med en backend, der kan bruges til at simulere HTTP-fejl enten tilfældigt eller systematisk. Her er, hvordan programmet ser ud:

RxJs prøveprogram

Konklusioner

Som vi har set, handler forståelsen af RxJs fejlhåndtering først og fremmest om at forstå det grundlæggende i Observable-kontrakten.

Vi skal huske på, at en given stream kun kan fejle én gang, og det er eksklusivt med stream completion; kun én af de to ting kan ske.

For at komme sig efter en fejl er den eneste måde at generere en erstatningsstream på en eller anden måde som et alternativ til den fejlbehæftede stream, som det sker i tilfældet med catchError- eller retryWhen-operatorerne.

Jeg håber, at du har nydt dette indlæg, hvis du gerne vil lære meget mere om RxJs, kan vi anbefale at tjekke kurset RxJs In Practice Course, hvor mange nyttige mønstre og operatører er dækket meget mere detaljeret.

Skriv et svar

Din e-mailadresse vil ikke blive publiceret.