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.
"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(); }, []);
}"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.