Jou app is gereed. Jy het 'n backend wat 'n paar magiese dinge doen en dan 'n paar data deur middel van 'n API blootstel. Jy het 'n frontend wat daardie API verbruik en die data aan die gebruiker toon. Jy gebruik die Fetch API om versoekings na jou backend te maak, dan die reaksie te verwerk en die UI te actualiseer. Wel, in ontwikkeling, ja. Dan implementeer jy jou app na produksie. En vreemde dinge begin gebeur. Die meeste van die tyd, alles lyk goed, maar soms misluk vrae. Die UI breek. Gebruikers klag. Jy wonder wat verkeerd gegaan het. Die netwerk is onvoorspelbaar, en jy moet daarvoor voorberei wees. Jy moet beter antwoorde hê op hierdie vrae: Wat gebeur wanneer die netwerk stadig of onbetroubaar is? Wat gebeur wanneer die backend af is of 'n fout retourneer? As jy eksterne API's verbruik, wat gebeur wanneer jy die koersgrens tref en geblokkeer word? Hoe hanteer jy hierdie scenario's gracieus en bied 'n goeie gebruikerservaring? Eerlik, vanilla Fetch is nie genoeg om hierdie scenario's te hanteer nie. Jy moet baie boilerplate kode byvoeg om foute, retries, timeouts, caching, ens te hanteer. In hierdie artikel sal ons ondersoek hoe om jou opname versoek produksie gereed maak met behulp van 'n biblioteek genaamd . We will: ffetch bou 'n backend met Node.js en Express met 'n paar eindpunte bou 'n frontend wat die eindpunte met vanille JavaScript met die Fetch API sondeer maak die agterkant flaky om werklike wêreld scenario's te simuleer see how fetch requests can fail and how to handle those failures implementeer ffetch om die beheer van fetch versoek te vereenvoudig en te verbeter Die Boilerplate Die backend sal RESTful eindpunte blootstel om gebruikers en take te skep, te lees, te actualiseer en te verwyder, en take aan gebruikers te toewys. die agterkant Ons sal Node.js en Express gebruik om die backend te bou. Ons sal ook 'n eenvoudige in-geheue data-opslag gebruik om dinge eenvoudig te hou. interface User { id: number; // Unique identifier name: string; // Full name email: string; // Email address } export interface Task { id: number; // Unique identifier title: string; // Short task title description?: string; // Optional detailed description priority: "low" | "medium" | "high"; // Task priority } Ons sal die volgende eindpunte skep: GET /gebruikers: Kry al die gebruikers (retourneer 'n reeks gebruikers-id's) : Create a new user (returns the created user's id) POST /users : Get a user by id (returns the user object) GET /users/:id PUT /users/:id: Opdater 'n gebruiker (terugkeer 'n suksesboodskap) DELETE /users/:id: Verwyder 'n gebruiker (terugkeer 'n suksesboodskap) GET /tasks: Kry al die take (terugkeer 'n reeks taak-id's) GET /tasks/:id: Kry 'n taak deur id (terugkeer die taak voorwerp) POST /tasks: 'n nuwe taak skep (gee die geskep taak-id terug) PUT /tasks/:id: Opdater 'n taak (returns 'n suksesboodskap) Delete /tasks/:id: Verwyder 'n taak GET /users/:userId/tasks: Kry al die take wat aan 'n gebruiker toegewy is (terugkeer 'n reeks taak-id's) POST /users/:userId/tasks/:taskId: Toekenning van 'n taak aan 'n gebruiker (returns 'n suksesboodskap) DELETE /users/:userId/tasks/:taskId: Verwyder 'n taak van 'n gebruiker (returns 'n suksesboodskap) die front Aangesien raamwerke gewoonlik hul eie abstraksies en maniere van dinge byvoeg, sal ons vanilla TypeScript gebruik om dinge eenvoudig en raamwerk-agnosties te hou. Ons sal 'n SPA met twee sienings skep: een vir die gebruikerslista en een vir 'n spesifieke gebruiker. Die gebruikerslista toon die gebruikersnaam en die aantal take wat aan hulle toegewyd word. Die klik op 'n gebruiker sal jou na die gebruikersweergave bring, wat die gebruikersdetails en hul take toon. Om dinge eenvoudig te hou, sal ons polling gebruik om die nuutste data van die backend te kry. Elke 3 sekondes, sal ons versoekings aan die backend maak om die nuutste data vir die huidige weergave te kry en die UI dienlik te actualiseer. Vir die gebruikerslijstweergave sal ons 'n versoek doen om to get all the user ids, then for each user, we will make a request to om hul besonderhede terug te kry, en bereken die aantal take wat aan hulle toegewy word. GET /users GET /users/:id GET /users/:id/tasks Vir die gebruiker view, sal ons 'n versoek doen om om die gebruiker se besonderhede te kry, en Om die taak-id's aan hulle toegewys te kry. Dan sal ons vir elke taak-id 'n versoek doen om to retrieve the task details. GET /users/:id GET /users/:id/tasks GET /tasks/:id Die GitHub Repo U kan die volledige kode vir hierdie voorbeeld in die begeleidende . GitHub repo Because of the amount of boilerplate, refer to the repo for the complete code. Every stage of the article will reference a branch in the repo. Die repo bevat beide die backend en frontend kode. verskyn, en die frontend is in die Wanneer jy die repo kloneer, loop in beide mappe om die afhanklikhede te installeer. Dan kan jy die backend met In die 'n Frontend en die Frontend met in the Die frontend sal dien word by and the backend at . backend frontend npm install npm run dev backend npm run dev frontend http://localhost:5173 http://localhost:3000 Once you have done all the chores and both your backend and frontend are running, you can open your browser and go to Om die app in aksie te sien: http://localhost:5173 In die ontwikkeling If you navigate to , moet jy sien alles werk net goed. As jy 'n nuwe gebruiker byvoeg met http://localhost:5173 curl -X POST http://localhost:3000/users \ -H "Content-Type: application/json" \ -d '{"name": "John Doe", "email": "john@example.com"}' jy moet sien die gebruiker verskyn in die gebruiker lys weergave binne 3 sekondes. Voel vry om rond te speel met die app en voeg meer gebruikers en take. Waarskynlik, alles sal goed werk. Wel, dit is waar ons uiteindelik by die punt van hierdie artikel kom. Ons backend werk net goed. Ons frontend, ten spyte van die verskriklike boilerplate, werk ook net goed. Maar tussen die frontend en backend is daar die netwerk. En die netwerk is onbetroubaar. Dus, laat ons sien wat gebeur as ons 'n bietjie flakiness by ons backend voeg. Netwerk foute simuleer Kom ons voeg 'n middleware by ons backend wat willekeurig misluk vrae met 'n 20% kans en voeg ook 'n paar willekeurige vertraging tot 1 sekonde. Jy kan die flaky middleware in die Hier is die kode: backend/src/middleware/flaky.ts import { Request, Response, NextFunction } from 'express'; export function flaky(req: Request, res: Response, next: NextFunction) { // Randomly fail requests with a 20% chance if (Math.random() < 0.2) { return res.status(500).json({ error: 'Random failure' }); } // Add random delay up to 2 seconds const delay = Math.random() * 2000; setTimeout(next, delay); } Dan kan ons hierdie middleware gebruik in ons Express-toepassing. Net importeer die middleware en gebruik dit voor jou roetes: backend/src/index.ts ... import { flaky } from './middleware/flaky'; ... app.use(cors()); app.use(express.json()); app.use(flaky); // Use the flaky middleware Hierdie kode is in die afdeling van die repo, sodat jy dit kan kyk met . network-errors git checkout network-errors Now, if you restart your backend and refresh the frontend, you should start seeing some weird things. The console will be filled with errors. Some fields on the UI will be En dit is wanneer, as jy nog nie het nie, jy moet begin dink oor hoe om hierdie foute gracieus te hanteer. undefined Error Scenarios Eerstens, laat ons identifiseer wat fout kan gaan en hoe ons dit kan hanteer: Intermittent network failures: requests can fail randomly, so on certain errors, we need to retry them a few times before giving up. By die polling stuur ons nie net een versoek nie, maar verskeie versoekings asynchrone. En 3 sekondes later stuur ons nog 'n batch versoekings. As 'n versoek van die vorige batch nog wag wanneer die volgende batch gestuur word, kan ons 'n vroeër reaksie kry na 'n latere. Dit kan lei tot 'n inkoherente UI-toestand. Ons moet seker maak dat slegs die nuutste reaksie gebruik word om die UI te actualiseer, dus wanneer 'n nuwe polling siklus begin, moet ons enige wagte versoekings van die vorige siklus annuleer. Net so, as die gebruiker na 'n ander weergave navigeer terwyl versoekings van die vorige weergave nog aan die gang is, kan ons antwoorde vir die vorige weergave kry nadat ons reeds weggejaag het. Dit kan ook lei tot 'n inkoherente UI-toestand. Ons moet seker maak dat slegs antwoorde vir die huidige weergave gebruik word om die UI te actualiseer, dus wanneer ons na 'n ander weergave navigeer, moet ons enige aan die gang komende versoekings van die vorige weergave annuleer. As 'n versoek op 'n sekere punt suksesvol was, maar dan misluk in 'n daaropvolgende opname siklus, wil ons nie dadelik 'n foutstatus aan die gebruiker wys nie. We have to handle scenarios where, say, we're viewing a user that has been deleted in the backend. We need to handle 404 errors gracefully and navigate back to the userlist view, or at least show a not found message. Also, we need to handle scenarios where the backend is completely down or unreachable. We need to show a global error message to the user and maybe retry the requests after some time. En die lys gaan voort, veral as die UI toelaat dat jy data skep, actualiseer of verwyder.Maar vir nou, laat ons fokus op die leesoperasies en hoe om foute te hanteer wanneer jy data kry. Handling Errors With Vanilla Fetch Hier, soos met baie dinge in JavaScript (of TypeScript), het jy twee opsies om hierdie scenario's te hanteer.Jy kan jou eie nutfunksie skryf om die Fetch API te wrap en die nodige foutbestuur logika by te voeg, of jy kan 'n biblioteek kies wat dit vir jou doen. Let's start with implementing everything ourselves. The code is on the afdeling van die repo, sodat jy dit kan kyk met . native-fetch git checkout native-fetch What Needs to Be Done Centralize all fetch logic in . poller.ts Vir elke opname, skep 'n nuwe AbortController, en annuleer die vorige. Wrap fetch calls in a retry-and-timeout function. On success, update a cache and use it for rendering. Op mislukking, herhaal as nodig, en hanteer timeouts / annulerings gracieus. Our Die lêer lyk nou so: poller.ts // Cache for responses const cache: Record<string, any> = {}; // AbortController for cancelling requests let currentController: AbortController | undefined; // Helper: fetch with retries and timeout async function fetchWithRetry(url: string, options: RequestInit = {}, retries = 2, timeout = 3000): Promise<any> { for (let attempt = 0; attempt <= retries; attempt++) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); try { const res = await fetch(url, { ...options, signal: controller.signal }); clearTimeout(timer); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); return data; } catch (err) { clearTimeout(timer); if (attempt === retries) throw err; } } } // Cancel all previous requests function cancelRequests() { if (currentController) currentController.abort(); currentController = new AbortController(); } export async function fetchUserListData() { cancelRequests(); // Use cache if available if (cache.userList) return cache.userList; try { if (!currentController) throw new Error('AbortController not initialized'); const userIds = await fetchWithRetry('http://localhost:3000/users', { signal: currentController!.signal }); const users = await Promise.all(userIds.map((id: number) => fetchWithRetry(`http://localhost:3000/users/${id}`, { signal: currentController!.signal }))); const taskCounts = await Promise.all(userIds.map((id: number) => fetchWithRetry(`http://localhost:3000/users/${id}/tasks`, { signal: currentController!.signal }).then((tasks: any[]) => tasks.length))); cache.userList = { users, taskCounts }; return cache.userList; } catch (err) { // fallback to cache if available if (cache.userList) return cache.userList; throw err; } } export async function fetchUserDetailsData(userId: number) { cancelRequests(); const cacheKey = `userDetails_${userId}`; if (cache[cacheKey]) return cache[cacheKey]; try { if (!currentController) throw new Error('AbortController not initialized'); const user = await fetchWithRetry(`http://localhost:3000/users/${userId}`, { signal: currentController!.signal }); const taskIds = await fetchWithRetry(`http://localhost:3000/users/${userId}/tasks`, { signal: currentController!.signal }); const tasks = await Promise.all(taskIds.map((id: number) => fetchWithRetry(`http://localhost:3000/tasks/${id}`, { signal: currentController!.signal }))); cache[cacheKey] = { user, tasks }; return cache[cacheKey]; } catch (err) { if (cache[cacheKey]) return cache[cacheKey]; throw err; } } Ons verwyder Soos al die fetch logika is nou in In ons vereenvoudigde gebruik geval, dit is draagbaar, maar selfs hier, het ons nodig gehad om baie boilerplate kode by te voeg om foute, retries, timeouts, annulerings en caching te hanteer. api.ts poller.ts As jy die app nou hardloop, moet jy sien dat dit baie beter werk. Die UI is meer stabiel en breek nie so dikwels nie. Jy kan nog steeds 'n paar foute in die konsole sien, maar hulle word gracieus hanteer en beïnvloed nie die gebruikerservaring soveel nie. Nadele van hierdie benadering Meer boilerplate kode: Ons moes 'n baie kode skryf om foute, retries, timeouts, annulerings en caching te hanteer. Not very reusable: The code is tightly coupled to our specific use case and not very reusable for other projects or scenarios. Limited features: The code only handles basic error scenarios. More complex scenarios like exponential backoff, circuit breakers, or global error handling would require even more code. Gebruik for Better Fetch Handling ffetch ffetch To address the downsides of our custom fetch handling, I wrote a library called . It is a small and lightweight library that wraps the Fetch API and provides a simple and declarative way to handle errors, retries, timeouts, cancellations, and some more features. ffetch Kom ons herskryf ons fetch logika gebruik . You can find the code on the branch of the repo, so you can check it out with . ffetch ffetch git checkout ffetch Eerstens die installasie in the folder: ffetch frontend npm install @gkoos/ffetch Then, we can rewrite our file using : poller.ts ffetch import createClient from '@gkoos/ffetch'; // Cache for responses const cache: Record<string, any> = {}; // Create ffetch client const api = createClient({ timeout: 3000, retries: 2, }); function cancelRequests() { api.abortAll(); } export async function fetchUserListData() { cancelRequests(); if (cache.userList) return cache.userList; try { const userIds = await api('http://localhost:3000/users').then(r => r.json()); const users = await Promise.all( userIds.map((id: number) => api(`http://localhost:3000/users/${id}`).then(r => r.json())) ); const taskCounts = await Promise.all( userIds.map((id: number) => api(`http://localhost:3000/users/${id}/tasks`).then(r => r.json()).then((tasks: any[]) => tasks.length)) ); cache.userList = { users, taskCounts }; return cache.userList; } catch (err) { if (cache.userList) return cache.userList; throw err; } } export async function fetchUserDetailsData(userId: number) { cancelRequests(); const cacheKey = `userDetails_${userId}`; if (cache[cacheKey]) return cache[cacheKey]; try { const user = await api(`http://localhost:3000/users/${userId}`).then(r => r.json()); const taskIds = await api(`http://localhost:3000/users/${userId}/tasks`).then(r => r.json()); const tasks = await Promise.all( taskIds.map((id: number) => api(`http://localhost:3000/tasks/${id}`).then(r => r.json())) ); cache[cacheKey] = { user, tasks }; return cache[cacheKey]; } catch (err) { if (cache[cacheKey]) return cache[cacheKey]; throw err; } } The code is much cleaner and easier to read. We don't have to worry about retries, timeouts, or cancellations anymore. Ons maak net 'n kliënt met die gewenste opsies en gebruik dit om versoekings te maak. ffetch Ander voordele van die gebruik ffetch Circuit breaker: automatic endpoint cooldown after repeated failures Automatiese eksponensiële back-off vir retries: verhoogde wagte tussen retries Global error handling: hooks for logging, modifying requests/responses, etc. Byvoorbeeld, ons kan kies om te herstel op netwerk foute en 5xx bediener foute, maar nie op 4xx kliënt foute. nie iets magies doen wat jy nie self kon bou nie, maar dit bespaar jou van skryf, toets en onderhou al daardie boilerplate. Dit is 'n gerief wrapper wat bak in produksie-graad patrone (soos circuit breaker en backoff) sodat jy kan fokus op jou app, nie jou vang logika. ffetch Conclusion Die belangrikste tip van hierdie artikel is nie dat jy moet gebruik spesifiek, maar dat jy nie moet vertrou op vanille Fetch vir produksie-voorbereide programme nie. Die netwerk is onbetroubaar, en jy moet voorberei wees vir dit. ffetch What you exactly need to do depends on your specific use case and requirements, but you can't go to production handling the happy path only. Things can and will go wrong, and your app needs to handle at least the most common failure scenarios. And can help with that. ffetch