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.
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.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:
try napotyka błądcatch z obiektem errorcatch obsługuje błąd (logowanie, naprawa)finally wykonuje operacje końcowe (czyszczenie zasobów)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.
JavaScript ma wbudowaną hierarchię błędów, podobnie jak park ma różne poziomy alertów dla różnych zagrożeń.
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.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.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 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.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.
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.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 bleduPierwsza 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ść.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); // 10000Wybieg 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.Dobra strategia obsługi błędów w aplikacji działa jak wielopoziomowy system bezpieczeństwa Parku Jurajskiego:
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.