Přeskočit na obsah

Validační throw před try/catch v async handleru — „Uložit" tiše nedělá nic

import { Aside } from ‘@astrojs/starlight/components’;

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”.

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 neawaituje

Throw 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.

  1. 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;
}
  1. Tolerantní parser vstupů: value.replace(/\s+/g, "").replace(",", ".")Number (\s pokrývá i NBSP   a ú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 throw v tělech async callbacků mezi začátkem funkce a prvním try {; a na onClick={() => 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) || 0 na 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í.
Přidal aiarchitekt.cz · 22. 7. 2026 2:00
Provozuje aiarchitekt.cz