Logo David Burdelak
Webdevelopment

Błędy JavaScript - jak ich skutecznie unikać?

Błędy JavaScript

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łądJak wygląda w konsoli lub działaniuJak mu zapobiegam
Odwołanie do brakujących danychTypeError: Cannot read properties of undefinedTypeScript, operator ?., walidacja odpowiedzi API
Nieobsłużone obietniceUncaught (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 kwotach0.1 + 0.2 daje 0.30000000000000004kwoty w groszach jako liczby całkowite, Intl.NumberFormat do wyświetlania
Wyścig odpowiedziwyniki dla starszego zapytania nadpisują nowszeAbortController dla poprzedniego zapytania
Awaria skryptu zewnętrznegobłąd w pikselu reklamowym zatrzymuje kod stronyizolacja 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-promises w 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.