Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Obsługa błędów w JavaScript

W Parku Jurajskim nawet najstaranniej zaplanowane systemy w końcu zawodzą - dinozaur wyłamuje się z wybiegu, czujnik przestaje raportować, automatyczny karmnik dostaje uszkodzone dane. Właśnie dlatego każdy system bezpieczeństwa parku ma swoje protokoły awaryjne: pierwsza usterka nie zamyka parku, tylko uruchamia procedurę. W JavaScript tą procedurą jest mechanizm try/catch/finally.

Blok try/catch - protokół bezpieczeństwa

Wyobraź sobie, że każda operacja w parku to potencjalne zagrożenie. Blok

try
jest strefą, w której wykonujemy ryzykowną operację, a
catch
ekipą ratunkową, która wkracza, gdy coś pójdzie nie tak. Podręcznikowym przykładem takiej ryzykownej operacji jest parsowanie JSON, bo transmisja z czujnika potrafi dotrzeć uszkodzona.

1try {
2  // Strefa ryzykowna - parsowanie danych z czujnika
3  const dinoData = JSON.parse('{ niepoprawny JSON }');
4  console.log(dinoData.species);
5} catch (error) {
6  // Ekipa ratunkowa - obsluga bledu
7  console.log("ALARM! Awaria systemu:", error.message);
8}
9console.log("System dziala dalej - park nie zostal zamkniety!");

Bez

try/catch
program zatrzymałby się na tym błędzie na dobre - tak jakby cały park trzeba było ewakuować z powodu jednego wadliwego czujnika. Tutaj błąd trafia do obiektu
error
, blok
catch
zapisuje jego komunikat w konsoli, a program kontynuuje działanie tuż za całą konstrukcją. Dlatego ostatnia linia mimo wszystko się wypisuje.

Przyjrzyj się temu, jaką drogą błąd w ogóle do nas dociera.

JSON.parse
nie zwraca żadnej wartości błędu, którą dałoby się porównać przez
===
, więc zapis w rodzaju
if (JSON.parse(data) === error)
nie ma szans zadziałać. Nie ma tu też callbacku
.onError()
, który można by zarejestrować, a wynik parsowania nie jest obietnicą (promise) - nie doczepisz do niego
.catch()
, a tym bardziej nie odwrócisz tego zapisu do postaci
error.catch(JSON.parse(data))
. Rzucony błąd potrafi przechwycić wyłącznie blok
catch
. Jedyny prawidłowy sposób obsługi błędu podczas parsowania JSON wygląda więc tak:
try { JSON.parse(data); } catch (error) { handleError(error); }
.

Sama konstrukcja zawsze ma ten sam kształt i składa się z pięciu części, wpisywanych dokładnie w tej kolejności: słowo kluczowe

try
, blok w klamrach z ryzykownym kodem
{ riskyCode(); }
, słowo kluczowe
catch
, parametr
(error)
w nawiasach, a na końcu blok obsługi
{ handleError(error); }
. Nawias z parametrem należy do
catch
, nigdy do
try
- to najczęstsza pomyłka przy pierwszych próbach. Zapamiętaj ten pięcioelementowy szkielet raz, a potem odtworzysz go z pamięci przy każdym ryzykownym wywołaniu.

Blok finally - protokół końcowy

finally
to blok, który wykonuje się zawsze - niezależnie od tego, czy błąd wystąpił, czy nie. To procedura domknięcia ogrodzenia: obowiązuje bez względu na wynik misji, a w kodzie odpowiada jej wszystko, o czym nie wolno zapomnieć nawet w panice.

1function feedDinosaur(dinoName, food) {
2  console.log("Otwieranie karmnika dla " + dinoName + "...");
3
4  try {
5    if (!food) {
6      throw new Error("Brak pozywienia w systemie!");
7    }
8    console.log("Podaje " + food + " dla " + dinoName);
9  } catch (error) {
10    console.log("ALARM: " + error.message);
11  } finally {
12    // ZAWSZE zamykamy karmnik - czy udalo sie nakarmic, czy nie
13    console.log("Zamykanie karmnika - bezpieczenstwo przede wszystkim!");
14  }
15}
16
17feedDinosaur("Rex", "mieso");  // Sukces + zamkniecie
18feedDinosaur("Rex", null);     // Blad + zamkniecie (finally dziala zawsze!)

Porównaj oba wywołania. Pierwsze karmienie kończy się sukcesem, więc

catch
w ogóle nie wchodzi do gry - a karmnik i tak zostaje zamknięty. Drugie rzuca błąd,
catch
ogłasza alarm - i karmnik znowu zostaje zamknięty. Tutaj rodzi się najczęstsze nieporozumienie:
finally
nie jest blokiem, który wykonuje się tylko gdy wystąpi błąd w bloku
try
, ani takim, który wykonuje się tylko gdy nie wystąpi żaden błąd, ani takim, który zależy od tego, czy blok
catch
pomyślnie obsłużył błąd. On przechodzi za każdym razem, również wtedy, gdy w
try
pojawi się
return
.

Cały cykl obsługi błędu przebiega przez pięć etapów:

  1. Kod w bloku
    try
    napotyka błąd
  2. Wykonanie przechodzi do bloku
    catch
    z obiektem
    error
  3. Blok
    catch
    obsługuje błąd (logowanie, naprawa)
  4. Blok
    finally
    wykonuje operacje końcowe (czyszczenie zasobów)
  5. Program kontynuuje działanie po bloku try/catch/finally

W czwartym etapie umieszczamy więc wszystko, co trzeba zwolnić bez względu na przebieg misji: połączenia, pliki i karmniki, które ktoś zostawił otwarte.

Klasy błędów w JavaScript

JavaScript ma wbudowaną hierarchię błędów, podobnie jak park ma różne poziomy alertów dla różnych zagrożeń.

Główne typy błędów

  • Error - ogólny błąd (alert ogólny w parku)
  • TypeError - operacja na niewłaściwym typie danych (karmienie roślinożercy mięsem)
  • RangeError - wartość poza dozwolonym zakresem (temperatura inkubatora poza skalą)
  • ReferenceError - odwołanie do nieistniejącej zmiennej (dinozaur, którego nie ma w bazie)
  • SyntaxError - błąd składni kodu (uszkodzony plan DNA)

Każdą z tych klas rozpoznasz wewnątrz

catch
operatorem
instanceof
i to właśnie ten test decyduje, którą procedurę awaryjną uruchomić - park musi wiedzieć, jaki alarm się włączył, zanim wyśle ekipę.

1// TypeError - operacja na niewlasciwym typie
2try {
3  const dino = null;
4  dino.roar(); // null nie ma metod!
5} catch (error) {
6  console.log(error instanceof TypeError); // true
7  console.log(error.message); // "Cannot read properties of null"
8}
9
10// RangeError - wartosc poza zakresem
11try {
12  const dinoArray = new Array(-5); // Ujemna dlugosc tablicy!
13} catch (error) {
14  console.log(error instanceof RangeError); // true
15}
16
17// ReferenceError - nieistniejaca zmienna
18try {
19  console.log(missingDinosaur);
20} catch (error) {
21  console.log(error instanceof ReferenceError); // true
22}

Trzy różne awarie, trzy różne klasy, a obsługuje je ten sam kształt

catch
. Właściwość
message
niesie opis czytelny dla człowieka, natomiast
instanceof
daje kategorię, na której program potrafi się rozgałęzić. Wszystkie trzy powstają w trakcie działania programu i dlatego
try/catch
w ogóle ma szansę je przechwycić.

SyntaxError
wyłamuje się z tej reguły, bo popsutą składnię we własnym pliku silnik wychwytuje jeszcze zanim program wystartuje. W czasie działania spotkasz ją w dwóch miejscach: wewnątrz
eval
oraz - znacznie częściej - przy parsowaniu uszkodzonego JSON.

1// SyntaxError - uszkodzony kod, zwykle wychwycony przed startem programu
2try {
3  eval("function(");
4} catch (error) {
5  console.log(error instanceof SyntaxError); // true
6}
7
8// Ta sama klasa wraca z uszkodzonego JSON
9try {
10  JSON.parse("{ niepoprawny JSON }");
11} catch (error) {
12  console.log(error instanceof SyntaxError); // true
13}

Drugi przykład domyka klamrę z naszym pierwszym fragmentem: alarm podniesiony przez

JSON.parse
to
SyntaxError
. Dzięki temu odróżnisz w kodzie transmisję, która przyszła uszkodzona, od transmisji, która dotarła w całości, ale zawiera bzdurne wartości. To dwie zupełnie różne awarie i wymagają dwóch różnych reakcji dyżurnego.

Throw - aktywacja alarmu

Słowo kluczowe

throw
pozwala celowo wywołać błąd - jak ręczne włączenie alarmu w parku w chwili, gdy zauważymy problem, zanim zdążą go wykryć systemy automatyczne. Towarzyszy mu jedna żelazna zasada: rzucaj obiekt
Error
albo jego podklasę, nigdy zwykły napis, bo tylko obiekt błędu niesie ze sobą komunikat, nazwę i ślad stosu wywołań.

1function checkEnclosureFence(voltage) {
2  if (voltage < 0) {
3    throw new RangeError("Napiecie nie moze byc ujemne: " + voltage);
4  }
5  if (voltage < 5000) {
6    throw new Error("ALARM: Napiecie ogrodzenia ponizej minimum! (" + voltage + "V)");
7  }
8  console.log("Ogrodzenie sprawne: " + voltage + "V");
9}
10
11try {
12  checkEnclosureFence(10000); // OK
13  checkEnclosureFence(3000);  // Rzuca Error
14} catch (error) {
15  console.log("Wykryto problem:", error.message);
16}

Dwa zabezpieczenia, dwie różne klasy: ujemny odczyt jest fizycznie niemożliwy, więc dostaje

RangeError
, a ogrodzenie tylko zbyt słabe - zwykły
Error
. W chwili, gdy
throw
zadziała, reszta funkcji zostaje porzucona: drugie wywołanie nigdy nie wypisze komunikatu o sprawnym ogrodzeniu, sterowanie skacze prosto do
catch
, a ewentualna trzecia instrukcja w bloku
try
też się nie wykona. Funkcja niczego nie rozstrzyga - ona tylko sygnalizuje, a decyzję podejmuje wywołujący. Ten sam mechanizm wykorzystasz przy walidacji danych: gdy funkcja wyszukująca nie znajdzie dinozaura, rzucenie
Error
jest znacznie bezpieczniejsze niż ciche zwrócenie
undefined
, a przy karmieniu każda złamana reguła - nieznana dieta, niemożliwy poziom zdrowia, pusty magazyn pożywienia - podnosi własny alarm.

Re-throw - przekazanie alarmu wyżej

Bywa, że

catch
przechwyci błąd, z którym sam nie potrafi nic zrobić. Wtedy wyrzuca go ponownie (re-throw), żeby zareagował ktoś wyżej w łańcuchu dowodzenia. Złapanie błędu tylko po to, by po cichu zniknął, jest najgorszym z możliwych rozwiązań: alarm milknie, ale dinozaur nadal biega na wolności.

1function processEnclosureData(data) {
2  try {
3    const parsed = JSON.parse(data);
4    if (!parsed.species) {
5      throw new Error("Brak gatunku w danych wybiegu");
6    }
7    return parsed;
8  } catch (error) {
9    if (error instanceof SyntaxError) {
10      // Ten przypadek umiemy obsluzyc - dane sa uszkodzone
11      console.log("Uszkodzone dane, uzywam wartosci domyslnych");
12      return { species: "Unknown", status: "error" };
13    }
14    // Kazdy inny blad - przekazujemy wyzej
15    throw error;
16  }
17}

Ta funkcja jest wybredna z premedytacją. Uszkodzony JSON daje

SyntaxError
, z którego potrafi się podnieść, podstawiając bezpieczny rekord zastępczy. Brak gatunku to zupełnie inna historia: dane sparsowały się poprawnie, więc zgadywanie zamiotłoby pod dywan prawdziwą usterkę w bazie parku - i dlatego ten błąd wędruje wyżej, do kodu, który ma dość kontekstu, by podjąć decyzję.
try/catch
ma filtrować błędy, a nie je pochłaniać. Połykanie wszystkiego w jednym pustym
catch
to przepis na usterki niewidoczne przez wiele miesięcy.

Własne klasy błędów

W Parku Jurajskim różne sytuacje wymagają wyspecjalizowanych procedur awaryjnych. W JavaScript dostajemy je, pisząc własne klasy błędów, które rozszerzają wbudowaną klasę

Error
.

1class DinosaurEscapeError extends Error {
2  constructor(species, enclosure) {
3    super("ALARM: " + species + " uciekl z wybiegu " + enclosure + "!");
4    this.name = "DinosaurEscapeError";
5    this.species = species;
6    this.enclosure = enclosure;
7  }
8}
9
10class FeedingError extends Error {
11  constructor(dinoName, reason) {
12    super("Blad karmienia " + dinoName + ": " + reason);
13    this.name = "FeedingError";
14    this.dinoName = dinoName;
15  }
16}
17
18class EnclosureVoltageError extends Error {
19  constructor(enclosure, voltage) {
20    super("Napiecie w wybiegu " + enclosure + ": " + voltage + "V (minimum 5000V)");
21    this.name = "EnclosureVoltageError";
22    this.enclosure = enclosure;
23    this.voltage = voltage;
24  }
25}

Przepis jest za każdym razem ten sam: konstruktor przekazuje gotowy komunikat do

super(...)
, ustawia czytelną nazwę w
name
, a potem zapisuje dodatkowe fakty potrzebne ekipie interwencyjnej - gatunek, numer wybiegu, zmierzone napięcie. Tą samą metodą dopiszesz
SensorError
dla czujnika, który przestał odpowiadać. Nic nie stoi też na przeszkodzie, żeby postawić nad nimi wspólną klasę nadrzędną, na przykład
ParkError extends Error
z polem
code
- wtedy pojedyncze sprawdzenie
instanceof ParkError
złapie całą rodzinę błędów parku naraz.

Gdy klasy są gotowe, obchód ochrony może reagować na każde zagrożenie inaczej:

1// Uzycie z selektywnym catch
2function parkSecurityCheck(enclosures) {
3  for (const enc of enclosures) {
4    try {
5      if (enc.voltage < 5000) {
6        throw new EnclosureVoltageError(enc.name, enc.voltage);
7      }
8      if (enc.dinoEscaped) {
9        throw new DinosaurEscapeError(enc.species, enc.name);
10      }
11      console.log("Wybieg " + enc.name + " - status OK");
12    } catch (error) {
13      if (error instanceof DinosaurEscapeError) {
14        console.log("CZERWONY ALERT: " + error.message);
15        console.log("Gatunek: " + error.species);
16      } else if (error instanceof EnclosureVoltageError) {
17        console.log("ZOLTY ALERT: " + error.message);
18      } else {
19        throw error; // Nieznany blad - re-throw
20      }
21    }
22  }
23}

Blok

catch
czytaj jak stanowisko dyspozytora: ucieczka dostaje czerwony alert razem z gatunkiem odczytanym prosto z obiektu błędu, słabe ogrodzenie żółty alert, a wszystko, czego dyspozytor nie rozpoznaje, wraca w górę zamiast być obsłużone na oślep. Ponieważ
try
siedzi w środku pętli, jeden felerny wybieg nie przerywa kontroli pozostałych - skaner idzie dalej. Właśnie własne klasy błędów umożliwiają taką precyzyjną obsługę: różne zagrożenia zasługują na różne procedury, dokładnie tak, jak w prawdziwym parku różne alarmy uruchamiają różne protokoły.

Optional chaining (?.) i Nullish coalescing (??)

Te dwa operatory to inteligentne czujniki parku. Pozwalają bezpiecznie poruszać się po danych, które bywają niekompletne, zamiast wysypywać centrum kontroli przy pierwszym brakującym polu.

Optional chaining (?.)

Operator

?.
sięga po zagnieżdżone właściwości bez ryzyka. Jeśli którakolwiek wartość po drodze okaże się
null
albo
undefined
, całe wyrażenie zatrzymuje się w tym miejscu i oddaje wynik, zamiast wywalić program.

1const dinoRecord = {
2  species: "Velociraptor",
3  tracker: {
4    lastLocation: "Sektor C",
5    gps: { lat: 23.5, lng: -80.2 }
6  }
7};
8
9// Bez optional chaining - ryzyko TypeError
10// const lat = dinoRecord.medical.lastCheckup.date; // TypeError!
11
12// Z optional chaining - bezpieczny dostep
13const lat = dinoRecord.medical?.lastCheckup?.date; // undefined (bez bledu!)
14const gpsLat = dinoRecord.tracker?.gps?.lat;       // 23.5
15
16// Dziala tez z metodami i indeksami
17const dinoCall = dinoRecord.sounds?.roar?.();      // undefined (metoda nie istnieje)
18const firstTag = dinoRecord.tags?.[0];             // undefined (brak tablicy)

Zatrzymaj się na linii

dinoRecord.medical?.lastCheckup?.date
. Nasz rekord nie ma właściwości
medical
, a mimo to wyrażenie zwróci
undefined
bez wyrzucenia błędu
: łańcuch urywa się na pierwszym brakującym ogniwie. Nie dostaniesz
null
, nie dostaniesz pustego stringa
""
i nie zobaczysz
TypeError
, choć
medical
faktycznie nie istnieje - dokładnie przed tym wyjątkiem chroni cię zakomentowana linia powyżej. Zapamiętaj też dwa warianty operatora:
?.()
wywoła metodę tylko wtedy, gdy ona istnieje, a
?.[0]
sięgnie po indeks tylko wtedy, gdy tablica naprawdę jest na miejscu. Miej przy tym świadomość, że
?.
ukrywa brak danych, ale go nie naprawia - milczący czujnik nadal milczy.

Nullish coalescing (??)

Operator

??
podstawia wartość domyślną, gdy lewa strona to
null
albo
undefined
. W odróżnieniu od
||
nie reaguje na
0
,
""
ani
false
- w odczytach parku to wartości najzupełniej prawidłowe.

1// Problem z || - traktuje 0 i "" jako puste
2const dinoWeight = 0;
3console.log(dinoWeight || "Brak danych");  // "Brak danych" (zle! 0 to prawidlowa waga)
4console.log(dinoWeight ?? "Brak danych");  // 0 (poprawnie!)
5
6// Praktyczny przyklad z danymi dinozaurow
7function getDinoInfo(dino) {
8  const name = dino?.name ?? "Nieznany gatunek";
9  const weight = dino?.weight ?? "Brak danych";
10  const isHealthy = dino?.isHealthy ?? false;
11  const speed = dino?.speed ?? 0;
12
13  return name + " | Waga: " + weight + "kg | Zdrowy: " + isHealthy + " | Predkosc: " + speed;
14}
15
16console.log(getDinoInfo({ name: "Rex", weight: 8000 }));
17console.log(getDinoInfo(null)); // Bezpieczne - nie wyrzuci bledu

Pierwsza para linii streszcza całą sprawę: świeżo wyklute pisklę dinozaura naprawdę może mieć zmierzoną wagę

0
, a
||
wyrzuciłoby ten pomiar do kosza i wypisało "Brak danych". Popatrz również na wywołanie
getDinoInfo(null)
- ono się nie wywala, bo
dino?.name
zwiera obwód jeszcze przed odczytem właściwości. Razem
?.
i
??
zamieniają awarię w sensowny raport zastępczy.

Ta sama ochrona liczy się przy konfiguracji czujników, gdzie świadomie wyłączona wartość jest pełnoprawną informacją, a nie luką do zasypania.

1const sensorConfig = {
2  voltage: 0,      // celowe zero
3  active: false,   // celowo wylaczony
4  label: ""        // celowo pusta nazwa
5};
6
7console.log(sensorConfig.voltage ?? 10000);     // 0 (jawne zero zostaje)
8console.log(sensorConfig.active ?? true);       // false (jawne false zostaje)
9console.log(sensorConfig.label ?? "bez nazwy"); // "" (jawny pusty string zostaje)
10console.log(sensorConfig.missing ?? "N/A");     // "N/A" (undefined, wiec wchodzi domyslna)

Tylko ostatnia linia sięga po wartość domyślną, bo tylko ostatniej właściwości naprawdę brakuje. Wyłączony czujnik zostaje wyłączony, ogrodzenie odcięte od prądu na czas przeglądu zostaje na zerze, a nienazwana sonda zachowuje pustą etykietę. Dokładnie tego oczekujesz od centrum kontroli, które nie ma prawa zmyślać odczytów - gdyby w tych czterech linijkach stało

||
, dyżurny zobaczyłby napięcie 10000V na ogrodzeniu, przez które spokojnie da się przejść.

Kombinacja ?. i ??

W praktyce oba operatory chodzą parami:

?.
bezpiecznie prowadzi do wartości, a
??
decyduje, co pokazać, gdy ta wartość nigdy nie dotrze.

1const parkData = {
2  enclosures: {
3    "A-1": { species: "T-Rex", voltage: 10000 }
4  }
5};
6
7// Bezpieczne odczytanie danych z wartoscia domyslna
8const voltage = parkData.enclosures?.["B-2"]?.voltage ?? "Brak czujnika";
9console.log("Napiecie B-2:", voltage); // "Brak czujnika"
10
11const rexVoltage = parkData.enclosures?.["A-1"]?.voltage ?? 0;
12console.log("Napiecie A-1:", rexVoltage); // 10000

Wybieg B-2 nie ma jeszcze wpisu w danych, więc łańcuch daje

undefined
, a
??
podstawia komunikat "Brak czujnika" - tablica dyżurnego melduje lukę w pokryciu, zamiast się zawiesić. Wybieg A-1 odpowiada prawdziwym odczytem i skoro
10000
nie jest ani
null
, ani
undefined
, wartość domyślna w ogóle nie wchodzi do gry. Jedna linia zastępuje tu całą kaskadę instrukcji
if
, a monitoring nie przerywa pracy przez jeden brakujący rekord.

Strategia obsługi błędów

Dobra strategia obsługi błędów w aplikacji działa jak wielopoziomowy system bezpieczeństwa Parku Jurajskiego:

  1. Waliduj dane wejściowe - sprawdzaj je, ZANIM staną się problemem (jak kontrola na bramce parku)
  2. Używaj konkretnych typów błędów - twórz własne klasy dla różnych sytuacji (inny alarm dla innego zagrożenia)
  3. Obsługuj błędy na właściwym poziomie - łap je tam, gdzie faktycznie umiesz je naprawić (zaciętego karmnika nie zgłasza się dyrektorowi parku)
  4. Przekazuj wyżej nieznane błędy - jeśli nie wiesz, jak obsłużyć błąd, oddaj go w górę łańcucha
  5. Zwalniaj zasoby w
    finally
    - zamykaj połączenia, pliki i ogrodzenia, cokolwiek się wydarzy

"Włączony alarm to nie katastrofa, tylko informacja" - powtarza Dr. Rex, zamykając panel kontrolny. Trzymaj się tych pięciu zasad, @name, i zapamiętaj sedno:

try/catch/finally
jest awaryjnym protokołem parku, własne klasy błędów są konkretnymi alarmami, które mówią ekipie, którą procedurę uruchomić, a
?.
i
??
to czujniki, dzięki którym nie wszczynasz alarmu bez powodu.

Przejdź do CodeWorlds