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");

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:

JavaScript definiuje kilka standardowych typów błędów dziedziczących po Error:

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:

  1. Sprawdza, czy oba argumenty a i b są liczbami. Jeśli nie, rzuca TypeError z odpowiednią wiadomością.
  2. Sprawdza, czy dzielnik b jest równy 0. Jeśli tak, rzuca Error z wiadomością "Nie można dzielić przez zero!".
  3. 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.