QA009highCorrectness

A failed read is rendered as an empty state

What it costs you

This loader turns a failed or unexpected response into an empty list and renders it, so the page shows the same thing it would show a brand-new account: no sessions, no scans, no members. Nothing tells the user the request failed, and anything they do next — including saving — acts on data the app never actually received.

The defect, and the fix

Both samples are scanned as app/settings/ApiKeysSection.tsx.

Our test suite runs both through the scanner on every build: the first must be reported, the second must not.

Reported
"use client";

export function ApiKeysSection() {
  const [keys, setKeys] = useState([]);

  const load = async () => {
    try {
      const res = await fetch("/api/api-keys");
      const data = await res.json();
      setKeys(data.keys || []);
    } catch {
      /* ignore */
    }
  };

  useEffect(() => { load(); }, []);
}
Not reported
"use client";

export function ApiKeysSection() {
  const [keys, setKeys] = useState([]);
  const [loadError, setLoadError] = useState(null);

  const load = async () => {
    const res = await fetch("/api/api-keys");
    if (!res.ok) {
      setLoadError("Could not load your API keys. Try again.");
      return;
    }
    const data = await res.json();
    setKeys(data.keys ?? []);
  };

  useEffect(() => { load(); }, []);
}

What changed: The loader checks the response and sets an error the page can render, so a failed read no longer looks like an account with no keys.

How to fix it

Check the response before using it: on a non-2xx, or a body without the field you expected, set an error state and render that instead of the empty state. Keep the `|| []` only for a response you have already confirmed arrived.

Why this rule doesn't cry wolf

Each clause below exists because it was attacked: someone was asked to find correct code that the rule would flag, and the clause is what stopped it. This is published because a check you cannot audit is a check you have to take on faith.

Client files only, and only a unit containing the literal global `fetch(` — throwing clients (ky, wretch, ofetch) reject on non-2xx, so `|| []` after them is correct, and in-file wrappers that check `.ok` and rethrow leave their callers containing no `fetch(` at all. The empty fallback must be `|| []`/`?? []`/`{}` over a PROPERTY READ passed straight to a `setX` state setter: `setRows(rows || [])` defaults a value the component already holds, and a pure helper's argument is never rendered. The unit is the effect callback, or the outermost non-component function — the nearest function cannot see the `.catch` below it, and the whole component would let an unrelated toast.error exonerate a broken loader. Exoneration is QA002's ERROR_PATH_SIGNALS with `.catch(` REMOVED: for a write a `.catch` means the author handled it, for a read `.catch(() => setLoaded(true))` is the defect itself. Measured at 6 fires over 5 sites in 437 real .tsx files with zero false positives, and silent on all three sites after they were fixed by hand.

This is not a security finding

QA009 is reported in its own section, separately from security findings. It does not change your security grade, and it does not fail your build unless you pass --fail-on-quality. The security catalogue lives at /rules.

Other Correctness checks

Check your own code

npx xploitscan scan .

Runs on every plan, including free. All 12 quality checks.