Async, Promises and error handling
The ZEUS frontend constantly talks to the API. Asynchronous code that handles errors poorly produces white screens and silent failures. This lesson tidies up the async/await patterns and error handling.
async/await instead of .then chains
// readable — async/await
async function loadProbes(): Promise<Probe[]> {
const res = await fetch("/api/probes");
if (!res.ok) {
throw new Error(`API ${res.status}`);
}
return res.json();
}
ProfessNet standard:
fetchdoes not throw an exception for 4xx/5xx statuses. We always checkres.okand throw an explicit error — otherwiseres.json()returns the error body as "data".
Parallelism
We run independent requests concurrently, not one after another.
// slow — sequential
const probes = await loadProbes();
const alerts = await loadAlerts();
// fast — in parallel
const [probes, alerts] = await Promise.all([loadProbes(), loadAlerts()]);
When some requests may fail and the rest should still proceed, we use
allSettled:
const results = await Promise.allSettled(forests.map(scanForest));
const ok = results.filter((r) => r.status === "fulfilled");
Error handling
Every await with I/O has its own try/catch or is wrapped higher up.
async function refresh() {
try {
const data = await loadProbes();
setProbes(data);
} catch (err) {
console.error("Failed to fetch probes", err);
toast.error("Error fetching data");
}
}
ProfessNet standard: we don't swallow errors silently. At minimum a log + a signal for the user (toast/message). An empty
catch {}is forbidden.
error is of type unknown
In TypeScript strict mode the error in catch has the type unknown — you
have to narrow it.
catch (err) {
const msg = err instanceof Error ? err.message : "Unknown error";
toast.error(msg);
}
Timeout and cancellation
Requests can hang. We use AbortController for timeouts and cleanup.
async function loadWithTimeout(url: string, ms = 10_000) {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), ms);
try {
const res = await fetch(url, { signal: ctrl.signal });
return await res.json();
} finally {
clearTimeout(t);
}
}
In useEffect, AbortController protects against updating an unmounted
component:
useEffect(() => {
const ctrl = new AbortController();
fetch("/api/probes", { signal: ctrl.signal })
.then((r) => r.json())
.then(setProbes)
.catch(() => {});
return () => ctrl.abort();
}, []);
| Pattern | For what |
|---|---|
await + res.ok | a single request with status checking |
Promise.all | many requests, all must succeed |
Promise.allSettled | many requests, some may fail |
AbortController | timeout and cleanup in useEffect |
Tip: in client components consider a library like TanStack Query — it gives you caching, retries, loading states and cancellation out of the box.
Check res.ok, catch errors, show them to the user and cancel hanging
requests. That's the difference between an app that "sometimes doesn't work"
and one you trust.