Większość błędów JavaScript, które trafiają do mnie z produkcji, nie wynika z trudnych algorytmów, tylko z kilku powtarzających się sytuacji: odwołania do danych, których nie ma, nieobsłużonej odpowiedzi z serwera, niejawnej konwersji typów albo skryptu zewnętrznego, który przestał działać i pociągnął za sobą resztę strony. W tym wpisie pokazuję błędy, które spotykam najczęściej, i nawyki oraz narzędzia, dzięki którym w moich projektach prawie nie wracają.
Najczęstsze błędy, które poprawiam
| Błąd | Jak wygląda w konsoli lub działaniu | Jak mu zapobiegam |
|---|---|---|
| Odwołanie do brakujących danych | TypeError: Cannot read properties of undefined | TypeScript, operator ?., walidacja odpowiedzi API |
| Nieobsłużone obietnice | Uncaught (in promise), przycisk, który „nic nie robi” | try/catch przy każdym await w akcjach użytkownika |
| Niejawna konwersja typów | "10" + 1 daje "101", puste pole formularza traktowane jak 0 | === zamiast ==, jawna zamiana typów |
| Obliczenia na kwotach | 0.1 + 0.2 daje 0.30000000000000004 | kwoty w groszach jako liczby całkowite, Intl.NumberFormat do wyświetlania |
| Wyścig odpowiedzi | wyniki dla starszego zapytania nadpisują nowsze | AbortController dla poprzedniego zapytania |
| Awaria skryptu zewnętrznego | błąd w pikselu reklamowym zatrzymuje kod strony | izolacja skryptów, ładowanie przez menedżera tagów, brak zależności od ich obecności |
Warto pamiętać, że JavaScript wiele sytuacji przepuszcza bez błędu. Dzielenie przez zero daje Infinity, odwołanie do nieistniejącej
właściwości obiektu daje undefined, a porównanie [] == false zwraca true. Takie błędy nie pojawiają się
w konsoli, tylko jako złe wyniki na stronie, więc ich wyłapanie wymaga testów i kontroli typów.
Kontrola typów i danych z zewnątrz
Największą zmianą w liczbie błędów w moich projektach było przejście na TypeScript. Kompilator wskazuje miejsca, gdzie wartość może być pusta, zanim kod trafi do przeglądarki. W mniejszych projektach bez etapu budowania korzystam przynajmniej z komentarzy JSDoc i sprawdzania typów w edytorze.
TypeScript nie chroni jednak przed danymi z zewnątrz. Odpowiedź API może mieć inną strukturę niż zakładana, a pole może zniknąć po zmianie po stronie serwera. Dlatego dane z sieci sprawdzam w jednym miejscu, zanim trafią do reszty aplikacji, np. biblioteką Zod:
import { z } from 'zod';
const Product = z.object({
id: z.number(),
name: z.string(),
price: z.number(), // w groszach
stock: z.number().default(0),
});
async function getProduct(id) {
const res = await fetch(`/api/products/${id}`);
if (!res.ok) throw new Error(`API ${res.status}`);
return Product.parse(await res.json()); // błąd tutaj, a nie w losowym miejscu interfejsu
}
Błędy w kodzie asynchronicznym
Najwięcej zgłoszeń typu „przycisk nie działa” wynika z nieobsłużonych błędów w funkcjach asynchronicznych. Zapytanie się nie udaje, wyjątek nie jest nigdzie złapany, a użytkownik nie dostaje żadnej informacji. Każda akcja użytkownika, która czeka na serwer, ma u mnie trzy stany: w trakcie, sukces i błąd, a ten ostatni zawsze kończy się komunikatem i możliwością ponowienia:
button.addEventListener('click', async () => {
button.disabled = true;
try {
await addToCart(productId);
showMessage('Dodano do koszyka.');
} catch (err) {
showMessage('Nie udało się dodać produktu. Spróbuj ponownie.');
reportError(err);
} finally {
button.disabled = false;
}
});
Osobny problem to kod, który działa poprawnie, ale zbyt długo blokuje przeglądarkę. Nie powoduje błędu w konsoli, a użytkownik widzi zawieszoną stronę. Jego źródła i sposoby rozbijania długich zadań wyjaśnia to, jak działa Event Loop w JavaScript.
Narzędzia, które łapią błędy przed wdrożeniem
- ESLint z regułami wykrywającymi nieużywane zmienne, porównania przez
==, nieobsłużone obietnice (no-floating-promisesw wersji dla TypeScript) i błędy w zależnościach hooków React. - Testy jednostkowe w Vitest lub Jest dla logiki, która liczy pieniądze, rabaty, koszty wysyłki czy daty, bo tam pomyłka ma bezpośredni koszt.
- Testy całych ścieżek w Playwright dla najważniejszych procesów: dodanie do koszyka, zamówienie, logowanie.
Wszystkie te kontrole uruchamiam automatycznie przy każdej zmianie w kodzie, tak żeby błąd został wykryty, zanim trafi do użytkowników. Taki zestaw to w praktyce automatyczne sprawdzanie szybkości i jakości kodu przed wdrożeniem, uruchamiane przy każdym pull requeście.
Błędy, które i tak trafią na produkcję
Żaden zestaw testów nie przewidzi wszystkich przeglądarek, rozszerzeń i danych użytkowników. Dlatego w każdym większym projekcie zbieram
błędy z przeglądarek użytkowników, np. w Sentry albo przez własny endpoint zasilany zdarzeniami error i
unhandledrejection. Raport pokazuje treść błędu, przeglądarkę, adres strony i kroki, które go poprzedziły, a mapy źródeł
pozwalają zobaczyć oryginalny kod zamiast zminifikowanego.
Gdy błąd jest trudny do odtworzenia, przechodzę do pracy z punktami przerwania i analizą wywołań w narzędziach deweloperskich. To, jak debugować kod JavaScript i PHP krok po kroku, zależy głównie od tego, czy błąd da się powtórzyć lokalnie.
Najskuteczniejszą ochroną przed błędami pozostaje jednak prosty kod. Krótkie funkcje z jednym zadaniem, czytelne nazwy i brak współdzielonego stanu zmienianego z wielu miejsc sprawiają, że błędy łatwiej zauważyć już przy przeglądzie, co jest podstawą tego, jak pisać czytelny i zrozumiały kod.