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: fetch does not throw an exception for 4xx/5xx statuses. We always check res.ok and throw an explicit error — otherwise res.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();
}, []);
PatternFor what
await + res.oka single request with status checking
Promise.allmany requests, all must succeed
Promise.allSettledmany requests, some may fail
AbortControllertimeout 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.