Validační throw před try/catch v async handleru — „Uložit" tiše nedělá nic
import { Aside } from ‘@astrojs/starlight/components’;
Symptom
Sekce “Symptom”Uživatel (skladová evidence, inventura) klikal na „Uložit” a nedělo se vůbec nic — žádná chybová hláška, žádný spinner, žádný request v Network tabu. Server logy čisté (request nikdy neodešel). Rozdělaný doklad navíc držel zámek (zmrazený sklad), takže blokoval navazující agendu ostatním. Problém trval dny, protože nebylo co diagnostikovat — „to tlačítko prostě nefunguje”.
Root cause
Sekce “Root cause”Dvě chyby v souběhu:
1) Validační throw mimo try/catch. Async handler stavěl payload PŘED try blokem; try/catch obaloval jen samotný fetch:
const save = useCallback(async () => { const payload = lines.map((line) => { const qty = parseLine(line); if (qty != null && !Number.isFinite(qty)) { throw new Error(`„${line.name}“: hodnota musí být číslo.`); // ← throw PŘED try } return { id: line.id, qty }; }); try { await apiSend(url, "PUT", { payload }); // ← catch chytá jen tohle } catch (e) { toast.error(...); }}, [...]);
// jinde: <Button onClick={() => save()} /> ← promise nikdo neawaitujeThrow z async funkce = rejected promise. onClick={() => save()} návratovou promise zahodí → unhandled rejection: hláška s názvem chybného řádku existovala, ale nikdy se nezobrazila.
2) Netolerantní parse čísel. Spouštěčem byla hodnota s mezerou jako oddělovačem tisíců — Number("1 216,5".replace(",", ".")) je NaN. Běžný lidský zápis množství (i NBSP z mobilní klávesnice) tak shodil celé uložení — kvůli jednomu řádku z 52.
Fix
Sekce “Fix”- Stavbu payloadu přesunout do čisté testovatelné funkce a volat ji uvnitř try/catch — chyba se VŽDY zobrazí toastem s názvem řádku:
let payload: Entry[];try { payload = buildPayload(lines, counts); // hází české chyby s názvem řádku} catch (e) { toast.error(e instanceof Error ? e.message : "Nelze uložit."); return false;}- Tolerantní parser vstupů:
value.replace(/\s+/g, "").replace(",", ".")→Number(\spokrývá i NBSPa úzkou NBSP); prázdný string a víc čárek = NaN, nikdy tiché|| 0(to by uložilo nulu místo chyby).
Jak se tomu vyvarovat v jiných systémech
Sekce “Jak se tomu vyvarovat v jiných systémech”- Detection: grep na
throwv tělech async callbacků mezi začátkem funkce a prvnímtry {; a naonClick={() => asyncFn()}bez.catch. Review otázka: „Co se stane, když tenhle throw vystřelí — kdo ho zobrazí?” - Anti-pattern:
const payload = …validace s throw…; try { await fetch }— validace a odeslání v různých zónách zachytávání. Druhý anti-pattern:Number(x) || 0na uživatelském vstupu (NaN tiše kolabuje na 0). - Lepší přístup: čistá
buildPayload()funkce (unit-testovatelná bez UI) + jediný try/catch přes CELÝ handler + globálníwindow.addEventListener("unhandledrejection", …)reportovaný do monitoringu, aby tichá selhání byla vidět.
Sister bugs / související
Sekce “Sister bugs / související”- Tolerance číselných vstupů: desetinná čárka vs. tečka, oddělovače tisíců (mezera/NBSP/úzká NBSP z mobilních klávesnic) — vstup, který „vypadá jako číslo”, musí buď projít, nebo dát adresnou chybu s názvem pole.
- Zámky/zmrazení odvozené od rozdělaného dokladu: pokud UI neumí doklad uložit, zámek visí a blokuje ostatní — u zámků vždy admin cestu na zrušení.