Lekcja 18: Obsługa błędów w JavaScript
Podczas pisania kodu nieuchronnie pojawiają się błędy. Mogą to być błędy składniowe (wykrywane przez interpreter przed uruchomieniem), błędy czasu wykonania (runtime errors, np. próba odwołania się do nieistniejącej zmiennej) lub błędy logiczne (kod działa, ale nie daje oczekiwanych rezultatów). W tej lekcji skupimy się na obsłudze błędów czasu wykonania za pomocą mechanizmów wbudowanych w JavaScript.
Blok try...catch
Podstawowym mechanizmem do obsługi błędów czasu wykonania jest blok try...catch
. Pozwala on "spróbować" wykonać fragment kodu, który potencjalnie może rzucić błąd, i "złapać" ten błąd, jeśli wystąpi, aby program mógł kontynuować działanie lub zareagować w odpowiedni sposób.
Składnia:
try {
// Kod, który może rzucić błąd
console.log("Początek bloku try");
// nieistniejacaFunkcja(); // To spowoduje błąd ReferenceError
let wynik = 10 / 0; // To spowoduje Infinity, nie błąd
if (wynik === Infinity) {
// Możemy sami rzucić błąd, jeśli warunek jest niepożądany
throw new Error("Dzielenie przez zero!");
}
console.log("Koniec bloku try (jeśli nie było błędu)");
} catch (error) {
// Kod wykonywany, jeśli w bloku try wystąpił błąd
console.error("Złapano błąd!");
console.error("Nazwa błędu:", error.name); // np. "ReferenceError", "Error"
console.error("Wiadomość błędu:", error.message); // np. "nieistniejacaFunkcja is not defined", "Dzielenie przez zero!"
// console.error("Stos wywołań:", error.stack); // Szczegółowe informacje o miejscu wystąpienia błędu
} finally {
// Kod wykonywany zawsze, niezależnie od tego, czy wystąpił błąd, czy nie
// Przydatne do operacji czyszczących (np. zamykanie plików, połączeń)
console.log("Blok finally (wykonywany zawsze)");
}
console.log("Kod po bloku try...catch...finally");
try
: Zawiera kod, który chcemy monitorować pod kątem błędów. Jeśli w tym bloku wystąpi błąd, wykonanietry
jest natychmiast przerywane, a sterowanie przechodzi do blokucatch
.catch (error)
: Wykonywany tylko wtedy, gdy w blokutry
zostanie rzucony błąd. Parametrerror
(nazwa jest dowolna, aleerror
lube
to konwencja) zawiera obiekt błędu z informacjami o nim (name
,message
,stack
).finally
(opcjonalny): Wykonywany zawsze po zakończeniu blokutry
(i ewentualniecatch
), niezależnie od tego, czy błąd wystąpił, czy nie, a nawet jeśli wtry
lubcatch
użytoreturn
,break
lubcontinue
.
Obiekt Error
Gdy występuje błąd czasu wykonania lub gdy jawnie rzucamy błąd za pomocą throw
, tworzony jest obiekt błędu, zazwyczaj instancja wbudowanego konstruktora Error
lub jednego z jego podtypów.
Najważniejsze właściwości obiektu Error
:
name
: Nazwa typu błędu (np. "Error", "SyntaxError", "ReferenceError", "TypeError").message
: Wiadomość opisująca błąd.stack
(niestandardowa, ale powszechnie dostępna): String zawierający stos wywołań funkcji prowadzący do miejsca wystąpienia błędu, bardzo pomocny przy debugowaniu.
JavaScript definiuje kilka standardowych typów błędów dziedziczących po Error
:
SyntaxError
: Błąd składniowy w kodzie.ReferenceError
: Próba odwołania się do nieistniejącej zmiennej.TypeError
: Operacja wykonana na wartości nieodpowiedniego typu (np. wywołanie metody naundefined
).RangeError
: Wartość liczbowa jest poza dopuszczalnym zakresem (np. przy tworzeniu tablicy o ujemnej długości).URIError
: Błąd związany z nieprawidłowym użyciem globalnych funkcji obsługujących URI (np.encodeURIComponent()
).
Instrukcja throw
Możemy sami generować (rzucać) błędy w naszym kodzie za pomocą instrukcji throw
. Pozwala to sygnalizować sytuacje wyjątkowe lub nieprawidłowe warunki, które powinny przerwać normalne wykonanie i zostać obsłużone.
Można rzucić dowolną wartość (liczbę, string, obiekt), ale najlepszą praktyką jest rzucanie instancji obiektu Error
lub jego podtypów, ponieważ zawierają one standardowe właściwości (name
, message
, stack
), które ułatwiają debugowanie i obsługę błędów.
function obliczPierwiastek(liczba) {
if (typeof liczba !== 'number') {
throw new TypeError("Argument musi być liczbą!");
}
if (liczba < 0) {
// Rzucenie standardowego obiektu Error z własną wiadomością
throw new Error("Nie można obliczyć pierwiastka kwadratowego z liczby ujemnej.");
}
return Math.sqrt(liczba);
}
try {
let wynik1 = obliczPierwiastek(16);
console.log("Pierwiastek z 16:", wynik1); // 4
// let wynik2 = obliczPierwiastek("abc"); // Rzuci TypeError
let wynik3 = obliczPierwiastek(-9); // Rzuci Error
console.log("Ten log się nie wykona");
} catch (error) {
console.error(`Wystąpił błąd [${error.name}]: ${error.message}`);
}
console.log("Program kontynuuje działanie po obsłużeniu błędu.");
Tworzenie własnych typów błędów
Czasami przydatne jest zdefiniowanie własnych, bardziej specyficznych typów błędów, aby lepiej kategoryzować problemy w aplikacji. Można to zrobić, tworząc klasy dziedziczące po wbudowanej klasie Error
.
// Definicja własnej klasy błędu
class BladWalidacji extends Error {
constructor(message) {
super(message); // Wywołanie konstruktora klasy nadrzędnej (Error)
this.name = "BladWalidacji"; // Ustawienie nazwy błędu
}
}
class BladApi extends Error {
constructor(message, status) {
super(message);
this.name = "BladApi";
this.status = status; // Dodanie własnej właściwości
}
}
function walidujEmail(email) {
if (!email || !email.includes('@')) {
throw new BladWalidacji(`Nieprawidłowy format adresu email: ${email}`);
}
console.log(`Email "${email}" jest poprawny.`);
}
async function pobierzDaneZApi(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new BladApi(`Błąd pobierania danych z API`, response.status);
}
return await response.json();
} catch (error) {
// Jeśli błąd pochodzi z fetch (np. sieciowy), opakowujemy go w BladApi
if (!(error instanceof BladApi)) {
throw new BladApi(`Błąd sieciowy lub inny: ${error.message}`, 503);
}
throw error; // Rzucamy dalej oryginalny BladApi
}
}
// Obsługa różnych typów błędów
try {
walidujEmail("test@example.com");
walidujEmail("niepoprawny-email"); // Rzuci BladWalidacji
} catch (error) {
if (error instanceof BladWalidacji) {
console.warn(`Problem z walidacją: ${error.message}`);
} else {
console.error(`Nieoczekiwany błąd: ${error.message}`);
}
}
(async () => {
try {
const dane = await pobierzDaneZApi("https://httpbin.org/status/404"); // Celowy błąd 404
console.log("Dane z API:", dane);
} catch (error) {
if (error instanceof BladApi) {
console.error(`Błąd API (status ${error.status}): ${error.message}`);
} else {
console.error(`Inny błąd API: ${error.message}`);
}
}
})();
Użycie operatora instanceof
w bloku catch
pozwala na różnicowanie obsługi w zależności od typu rzuconego błędu.
Zadanie praktyczne
Napisz funkcję dziel(a, b)
, która:
- Sprawdza, czy oba argumenty
a
ib
są liczbami. Jeśli nie, rzucaTypeError
z odpowiednią wiadomością. - Sprawdza, czy dzielnik
b
jest równy 0. Jeśli tak, rzucaError
z wiadomością "Nie można dzielić przez zero!". - Jeśli warunki są spełnione, zwraca wynik dzielenia
a / b
.
Następnie wywołaj tę funkcję w bloku try...catch
, testując przypadki poprawnego dzielenia, dzielenia przez zero i przekazania nie-liczby jako argumentu. W bloku catch
wyświetl nazwę i wiadomość błędu.
Pokaż rozwiązanie
function dziel(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError("Oba argumenty muszą być liczbami.");
}
if (b === 0) {
throw new Error("Nie można dzielić przez zero!");
}
return a / b;
}
// Testowanie
console.log("Testowanie funkcji dziel:");
// Przypadek 1: Poprawne dzielenie
try {
let wynik1 = dziel(10, 2);
console.log("Wynik 10 / 2:", wynik1);
} catch (error) {
console.error(`[${error.name}] ${error.message}`);
}
// Przypadek 2: Dzielenie przez zero
try {
let wynik2 = dziel(5, 0);
console.log("Wynik 5 / 0:", wynik2); // Ten log się nie wykona
} catch (error) {
console.error(`[${error.name}] ${error.message}`);
}
// Przypadek 3: Nieprawidłowy typ argumentu
try {
let wynik3 = dziel("abc", 3);
console.log("Wynik 'abc' / 3:", wynik3); // Ten log się nie wykona
} catch (error) {
console.error(`[${error.name}] ${error.message}`);
}
console.log("Zakończono testowanie.");
Zadanie do samodzielnego wykonania
Stwórz własną klasę błędu BrakDostepuError
dziedziczącą po Error
. Następnie napisz funkcję sprawdzDostep(uzytkownik)
, która przyjmuje obiekt użytkownika (np. { nazwa: "Admin", rola: "admin" }
lub { nazwa: "User", rola: "user" }
). Jeśli rola użytkownika nie jest "admin", funkcja powinna rzucić instancję BrakDostepuError
z odpowiednią wiadomością. W przeciwnym razie powinna wyświetlić komunikat o przyznaniu dostępu.
Przetestuj funkcję w bloku try...catch
, obsługując specyficznie błąd BrakDostepuError
.
FAQ - Obsługa błędów w JavaScript
Czy muszę zawsze używać `try...catch`?
Nie zawsze. Używaj try...catch
głównie wokół kodu, który może rzucić błąd, a Ty chcesz ten błąd obsłużyć w kontrolowany sposób, np. wyświetlić użytkownikowi przyjazny komunikat, zapisać logi lub spróbować wykonać operację ponownie. Nieobsłużone błędy zatrzymają wykonanie bieżącego skryptu (lub funkcji async).
Jaka jest różnica między `Error` a `Exception`?
W JavaScript terminologia jest nieco inna niż w niektórych innych językach (jak Java czy C#). W JS mówimy o "rzucaniu" (throwing) i "łapaniu" (catching) błędów (Errors). Termin "Exception" (wyjątek) jest często używany zamiennie z "Error", ale formalnie standard ECMAScript definiuje obiekt Error
jako podstawowy typ dla błędów czasu wykonania.
Czy `finally` wykona się nawet jeśli zamknę przeglądarkę?
Nie. Blok finally
gwarantuje wykonanie w kontekście normalnego przepływu sterowania programu JavaScript, w tym przy instrukcjach return
, break
, continue
czy nieprzechwyconych błędach wewnątrz catch
. Nie gwarantuje wykonania przy nagłym zakończeniu środowiska wykonawczego (np. zamknięcie karty przeglądarki, awaria systemu).
Czy mogę mieć wiele bloków `catch` dla jednego `try`?
Nie bezpośrednio, jak w niektórych innych językach. W JavaScript blok try
może mieć tylko jeden blok catch
. Aby obsłużyć różne typy błędów w różny sposób, używa się instrukcji warunkowych (np. if...else if...else
) wewnątrz pojedynczego bloku catch
, sprawdzając typ błędu za pomocą instanceof
lub właściwości error.name
.
Kiedy powinienem tworzyć własne typy błędów?
Tworzenie własnych typów błędów jest przydatne, gdy chcesz odróżnić specyficzne błędy aplikacji od ogólnych błędów JavaScript. Ułatwia to logikę obsługi błędów, pozwalając na bardziej precyzyjne reagowanie na konkretne sytuacje wyjątkowe w Twoim kodzie (np. błędy walidacji, błędy API, błędy braku uprawnień).
Co to jest "unhandled promise rejection" i jak się ma do `try...catch`?
"Unhandled promise rejection" występuje, gdy obietnica (Promise) zostaje odrzucona, ale nie ma dołączonego bloku .catch()
(ani drugiego argumentu w .then()
) do obsłużenia tego odrzucenia. Standardowy blok try...catch
nie złapie takiego błędu, chyba że używasz await
na tej obietnicy wewnątrz bloku try
w funkcji async
.