Blogg

Här finns tekniska artiklar, presentationer och nyheter om arkitektur och systemutveckling. Håll dig uppdaterad, följ oss på LinkedIn

Callista medarbetare Marcus Cedergren

Konsten att misslyckas snyggt - Att designa för resiliens i webbutveckling

// Marcus Cedergren

Illustration av en utvecklare vid en kontrollpanel som justerar inställningar för att upprätthålla dataflödet till en familj som använder sina enheter under en långsam anslutning

Inget system är perfekt. Servrar går ner, nätverk svajar på pendeln och externa API:er slutar svara. Skillnaden mellan en instabil applikation och en professionell, förtroendeingivande upplevelse ligger sällan i om felen uppstår – utan i hur systemet hanterar dem.

Genom att acceptera att fel är oundvikliga kan vi lättare designa system som aldrig lämnar användaren i ett vakuum. Att integrera detta perspektiv i utveckling från dag ett höjer inte bara den tekniska kvaliteten - det bygger den typ av tillit som krävs för att behålla användare när tekniken sviker.

Begreppet resiliens är ett brett ämne och en systemisk egenskap som spänner över hela stacken – från databaser och mikrotjänster till lastbalanserare och infrastruktur. I det här inlägget kommer vi dock fokusera specifikt på vad man bör tänka på för frontend-delen av en lösning.

Glappet mellan labbmiljö och verklighet

Frontend-utveckling sker ofta i en skyddad verkstad: på en kraftfull MacBook Pro med gigabitlina och minimal latens som körs mot lokala tjänster. Detta skapar lätt en falsk trygghet kring applikationens robusthet. I själva verket är nätverk och externa tjänster opålitliga faktorer som kräver en proaktiv strategi. Om man tänker stödja en användare med en budgetmobil på en skakig uppkoppling, inser man snabbt att “happy path”-design inte räcker.

Illustration av kontrasten mellan en stabil utvecklingsmiljö och användarens verklighet med dålig nätverksuppkoppling.

Få saker skadar användarupplevelsen så mycket som tystnad när något går fel. Den där känslan av att ha “fastnat” på en sida beror sällan på slumpen, utan på att koden inte är förberedd på verklighetens nätverksfel eller instabila backends. Genom att kommunicera vad som händer förvandlar du ett tekniskt problem till en hanterbar situation som bygger snarare än river ner förtroende.

Explorativ testning i DevTools: Att provocera fram sanningen

Istället för att förlita sig på att felhanteringen täcker alla scenarier, kan man använda Chrome DevTools (eller motsvarande) som ett litet “kaos-laboratorium” för att praktiskt provocera fram många olika typer av feltillstånd.

Illustration av en utvecklare som stresstestar ett system genom att vrida på kontroller för throttling tills datorn ryker.

Här är några funktioner i DevTools jag använder nästan dagligen för detta:

  • Simulering av nätverksbegränsningar: Genom att ställa om nätverket (under Network -> Throttling) till t.ex. “Slow 3G” eller en egen definition med låg bandbredd och hög latens blir det tydligt om applikationen ger tillräcklig feedback under långa väntetider.

  • Local Overrides för kontrollerade feltillstånd: Att använda Local Overrides i DevTools är som att ha en fjärrkontroll till verkligheten. Du kan tvinga systemet att hantera scenarier som annars är svåra att återskapa utan att röra en rad kod i din backend. Du kan enkelt ändra responsen (både body och headers) från ett API direkt i webbläsaren. Experimentera enkelt för att se vad som händer om man får extremt många resultat eller andra edge-cases. Local overrides använder jag för övrigt ofta för att kunna utveckla frontend-delen av en feature innan backend-delen är klar vilket gör att man kan jobba parallellt - givet att man kommit överens om API:et såklart.

  • Request Blocking - när endpoints inte svarar: Jag brukar också testa “Request Blocking” för olika anrop och resurser för att se hur appen beter sig om en API-endpoint inte svarar eller ett kritiskt tredjepartsskript inte kan laddas. Det är ett otroligt mångsidigt och lättviktigt sätt att testa hur en sida hanterar olika fall utan att behöva instrumentera en backend för olika scenarier.

  • Prestandabegränsningar: Genom att begränsa CPU-kapaciteten (under Performance-fliken) kan du kontrollera att gränssnittet inte låser sig eller är oacceptabelt segt på enheter med begränsade resurser.

  • Verifiera Graceful Degradation med Device Mode: Utöver nätverk och API-svar är den fysiska enhetens begränsningar en av de största osäkerhetsfaktorerna. Att en sida ser bra ut på en stor skärm betyder inte att den är funktionell i handen på en användare. Genom Device Mode kan du simulera specifika förutsättningar bortom att bara “dra i fönstret”. Det är ett ovärderligt verktyg för att testa Graceful Degradation i praktiken: när skärmytan krymper eller hårdvaran blir svagare måste vi prioritera vad som är absolut mest kritiskt. Vissa förespråkar att man istället arbetar utifrån motsatt håll, dvs. Progressive Enhancement. Oavsett strategi är målet detsamma: att säkerställa att viktiga återkopplingar och felmeddelanden alltid är synliga och att appen förblir användbar även under press.

Jag brukar använda följande process för att verifiera en ny feature:

  1. Explorativ fas: Använd DevTools för att aktivt leta efter brister och provocera fram fel. Det är här du upptäcker var och hur upplevelsen faktiskt går sönder för användare.
  2. Förbättra: Försök åtgärda bristerna i koden. Det kan handla om att få till en bra strategi för hur långt ett fel propagerar innan det hanteras (Error Boundaries), att använda skeleton-content eller någon form av förloppsindikator, retry-mekanismer och tydligare vägar framåt för användaren.
  3. Regressionstestning: När du väl har identifierat ett felscenario / felhantering som bedöms vara kritiskt för appen bör du överväga att införa en verifiering i form av ett test. I verktyg som Playwright finns t.ex. den mycket användbara funktionen route.fulfill för att i dina tester simulera exakt det scenario du nyss hittade manuellt.

Arkitektoniska strategier för resiliens

När brister identifieras genom testning landar jag ofta i att det behövs mer än bara ett felmeddelande. Några användbara strategier man kan tillämpa för att förbättra resiliensen:

  1. Graceful Degradation: Om en icke-kritisk tjänst (som en chatt-widget eller en analysmodul) dör, ska resten av applikationen förbli användbar. Genom att isolera felen i exempelvis Error Boundaries kan vi se till att en trasig detalj inte sänker hela skeppet, utan att applikationen degraderas värdigt istället för att krascha totalt.

  2. Retry-logik med Exponential Backoff: Vid tillfälliga fel räcker det inte alltid att bara prova igen direkt. Man bör vänta successivt längre mellan försöken (t.ex. 500ms, 1s, 4s) för att ge en ansträngd server eller ett svajigt nätverk chansen att återhämta sig istället för att stressa systemet ytterligare genom att “hamra” på en stängd dörr. Här kan man också med fördel använda sig av en fördröjd notifiering, dvs. om ett anrop under normala omständigheter tar < 500 ms kan man vänta med att visa spinners / status osv tills en viss tid förflutit för att minska ryckigheten i användarupplevelsen.

  3. Fallback-strategier med lokal cache: När nätverket svajar eller ett anrop misslyckas bör applikationen ha en plan B. Det kan vara att visa senast kända data från en lokal cache (t.ex. via localStorage eller IndexedDB) samtidigt som vi försöker hämta ny information i bakgrunden. Att presentera innehåll som är någon minut gammalt är ofta en bättre upplevelse än att möta användaren med en stum felsida, men beror såklart på vilken typ av applikation det handlar om. För vissa applikationer kan till och med Local First vara ett intressant alternativ som helt eller delvis eliminerar de problem vi tar upp här.

Telemetri: Att stänga loopen

Resiliens är såklart inget statiskt tillstånd eller egenskap som du säkerställer en gång och sen kan glömma bort, utan en kontinuerlig process. Även om vi designar och kodar för osäkerhet kommer användare i verkligheten alltid att stöta på felkombinationer vi inte kunnat förutse. Utan en centraliserad överblick förblir dessa klientfel “osynliga”, och användarens frustration når aldrig fram till oss.

Här blir telemetri avgörande för att stänga loopen. Genom att fånga upp ohanterade undantag och nätverksfel förvandlas varje misslyckande från en död punkt till värdefull data. Det gör att vi kan agera proaktivt och laga hålen i systemet innan de hinner bli kritiska supportärenden.

Att implementera detta behöver inte vara komplicerat och kan vara så enkelt som att posta en felrapport till ett dedikerat API i din backend som sedan sparar felrapporten på lämpligt sätt. Alternativt implementerar man en fullfjädrad “plug-and-play”-tjänst som Sentry, Rollbar eller Raygun som sköter allt från felrapportering till gruppering och analys av prestandadata åt dig.

Sammanfattning

Att designa för resiliens handlar om att acceptera att fel kommer att uppstå och att planera för dem. Genom att kombinera manuellt utforskande i DevTools med riktad automation (exempelvis felscenarier i Playwright) och arkitektoniska mönster, får du applikationer som hanterar motgångar bättre och förhoppningsvis åtminstone misslyckas snyggt.

Tack för att du läser Callistas blogg.
Hjälp oss att nå ut med information genom att dela nyheter och artiklar i ditt nätverk.

Kommentarer