Try-Catch: istota, konstrukcja przechwytywania wyjątków i jak działa w programowaniu mobilnym

Autor: IT Sectr Opublikowano: 2026-05-25 Czas czytania: 9 min

Try-Catch — konstrukcja przechwytywania wyjątków, pozwalająca wykonywać potencjalnie niebezpieczny kod w chronionym bloku i poprawnie obsługiwać błędy bez awaryjnego zakończenia programu. Blok try zawiera kod, który może zgłosić wyjątek, catch przechwytuje go i wykonuje logikę odzyskiwania. Według Apple Swift Documentation (2026), blok finally jest wykonywany niezależnie od tego, czy wyjątek został zgłoszony, czy nie, gwarantując zwolnienie zasobów.

Najważniejsze

  • Try-Catch — konstrukcja do przechwytywania wyjątków, zapobiegająca awaryjnemu zakończeniu programu przy błędach
  • Blok try zawiera kod, który może zgłosić wyjątek — wykonanie przerywa się przy pierwszym błędzie
  • Blok catch przechwytuje wyjątek określonego typu i wykonuje logikę obsługi lub odzyskiwania
  • Blok finally gwarantuje wykonanie po try/catch w celu zwolnienia zasobów, np. zamknięcia plików
  • Zagnieżdżone try-catch pozwalają obsługiwać błędy na różnych poziomach abstrakcji wewnątrz jednej funkcji

Co to jest Try-Catch?

Try-Catch — fundamentalna konstrukcja strukturalnej obsługi wyjątków, obecna w większości nowoczesnych języków programowania. Składa się z trzech bloków: try (próba wykonania niebezpiecznego kodu), catch (przechwycenie i obsługa wyjątku) oraz opcjonalnego finally (finalizacja). Ideą konstrukcji jest oddzielenie logiki operacji biznesowych od logiki obsługi błędów, czyniąc kod bardziej czytelnym i przewidywalnym.

Koncepcja została po raz pierwszy zaimplementowana w języku C++ jako try/catch, następnie zapożyczona przez Java, C#, Swift, Kotlin, Dart, Python, JavaScript i inne języki. Każdy język dodaje własne cechy: w Swift blok catch musi być wyczerpujący, w Kotlin try-catch może być wyrażeniem, w Dart finally jest obowiązkowy dla zasobów strumieniowych. Pomimo różnic, podstawowa zasada jest jedna: błąd jest obsługiwany jak najbliżej miejsca wystąpienia, a nie globalnie.

Używanie Try-Catch jest szczególnie istotne w programowaniu mobilnym, gdzie czynniki zewnętrzne — utrata sieci, nieprawidłowa odpowiedź serwera, brak pamięci — występują stale. Prawidłowa obsługa wyjątków zapobiega crashom aplikacji i zapewnia odpowiedni UX: użytkownik otrzymuje komunikat o błędzie zamiast nagłego zamknięcia aplikacji. Według Google Android Kotlin Style Guide (2026), każda funkcja, która może zgłosić wyjątek, powinna go obsłużyć przez try-catch albo zadeklarować throws w sygnaturze.

Jak działa Try-Catch

Mechanizm wykonania try-catch opiera się na rozwinięciu stosu (stack unwinding). Gdy wewnątrz bloku try zostanie zgłoszony wyjątek przez operator throw (lub w wyniku błędu systemowego), normalny przepływ wykonania zostaje natychmiast przerwany. Wykonanie przemieszcza się na wyższy poziom stosu wywołań w poszukiwaniu odpowiedniego bloku catch. Nowoczesne języki szukają catch z typem odpowiadającym typowi zgłoszonego wyjątku, używając mechanizmu dopasowania typów (type matching).

Jeśli odpowiedni catch zostanie znaleziony, wykonywane jest jego ciało, po czym wykonanie kontynuowane jest po całej konstrukcji try-catch-finally. Jeśli catch nie zostanie znaleziony, wyjątek jest przekazywany dalej w górę stosu i może być obsłużony na wyższym poziomie — aż do globalnego handlera, który w aplikacji mobilnej pokazuje użytkownikowi okno dialogowe z błędem. Jeśli wyjątek nie zostanie obsłużony nigdzie, aplikacja ulega awaryjnemu zakończeniu. Dlatego prawidłowa obsługa wszystkich możliwych typów wyjątków jest krytyczna dla stabilności aplikacji.

Składnia podstawowej konstrukcji

kotlin
fun readUserData(): User {
    return try {
        val response = api.fetchUser()
        parseUser(response)
    } catch (e: IOException) {
        logError("Błąd sieci", e)
        throw AppException("Nie udało się załadować danych")
    } catch (e: JsonParseException) {
        logError("Błąd parsowania", e)
        return User.default()
    } finally {
        closeLoadingIndicator()
    }
}

Kod najpierw próbuje wykonać zapytanie do API i sparsować odpowiedź. Jeśli wystąpi IOException (problem z siecią), wyjątek jest logowany i przekazywany dalej jako AppException. Jeśli JsonParseException — zwracany jest użytkownik domyślny. Blok finally gwarantuje ukrycie wskaźnika ładowania, zapobiegając wyciekowi komponentów UI na ekranie.

Try-Catch w Swift

W Swift obsługa błędów jest zaimplementowana przez protokół Error (wcześniej ErrorType). Każdy typ zgodny z Error może być zgłoszony przez operator throw. Funkcja, która może zgłosić błąd, jest oznaczona słowem kluczowym throws w sygnaturze. Wywołanie takiej funkcji wymaga prefiksu try (dla jawnego try-catch), try? (opcjonalny wynik) lub try! (wymuszone wykonanie bez obsługi błędu).

swift
enum NetworkError: Error {
    case noConnection
    case serverError(code: Int)
    case timeout
}

func fetchUser(id: Int) throws -> User {
    guard isConnected() else {
        throw NetworkError.noConnection
    }
    let data = try performRequest(path: "/users/\(id)")
    return try decodeUser(from: data)
}

do {
    let user = try fetchUser(id: 42)
    updateUI(user)
} catch NetworkError.noConnection {
    showOfflineAlert()
} catch let error as NetworkError {
    showError("Sieć " + error.localizedDescription)
} catch {
    showGenericError()
}

Enum NetworkError implementuje protokół Error, definiując trzy przypadki: noConnection, serverError z kodem i timeout. Funkcja fetchUser jest oznaczona throws: najpierw sprawdza połączenie, następnie wykonuje zapytanie i parsowanie. W bloku do-catch trzy catch obsługują różne scenariusze: specyficzny przypadek noConnection, ogólny typ NetworkError i wszystkie pozostałe błędy. Pozwala to pokazywać użytkownikowi różne komunikaty w zależności od typu problemu.

Try-Catch w Kotlin

Kotlin odziedziczył try-catch-finally po Java, ale dodał ważną różnicę: w Kotlin try-catch jest wyrażeniem (expression), a nie instrukcją (statement). Oznacza to, że wynik bloku try lub catch można przypisać do zmiennej. Ostatnie wyrażenie w bloku try staje się wynikiem przy sukcesie, ostatnie wyrażenie w catch — przy błędzie. Jeśli błąd nie zostanie obsłużony przez żaden catch, wyjątek jest przekazywany wyżej w stosie.

kotlin
sealed class Result<out T> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Throwable) : Result<Nothing>()
}

fun loadData(): Result<List<Item>> {
    return try {
        val response = api.getItems()
        Result.Success(response.toList())
    } catch (e: HttpException) {
        Log.e("HTTP ", e)
        Result.Error(e)
    } catch (e: IOException) {
        Log.e("Sieć ", e)
        Result.Error(e)
    }
}

W przykładzie sealed class Result opakowuje udaną odpowiedź lub błąd. Funkcja loadData używa try-catch jako wyrażenia: przy sukcesie zwraca Result.Success, przy wyjątku HttpException lub IOException — Result.Error z logowaniem. Takie podejście pozwala stronie wywołującej obsługiwać błędy bez wyjątków — przez when-wyrażenie po typie Result. Jest to szczególnie wygodne w Jetpack Compose do wyświetlania różnych stanów UI (Loading, Success, Error) przez StateFlow i collectAsState.

Try-Catch w Dart i Flutter

Dart obsługuje try-catch-finally ze składnią podobną do Java, ale z dodatkiem klauzuli on do filtrowania po typie wyjątku bez podawania zmiennej. Jest to wygodne, gdy sam wyjątek nie jest potrzebny — ważny jest tylko fakt jego typu. Dart obsługuje również blok catch z dwoma parametrami: obiekt wyjątku i StackTrace, co jest przydatne do logowania pełnego łańcucha wywołań.

dart
import 'dart:io';
import 'dart:convert';

class UserRepository {
    Future<User> fetchUser(String id) async {
        try {
            final client = HttpClient();
            final request = await client.getUrl(
                Uri.parse('https://api.example.com/users/$id')
            );
            final response = await request.close();
            final body = await response.transform(utf8.decoder).join();
            return User.fromJson(json.decode(body));
        } on SocketException catch (e, stackTrace) {
            log("No internet", e, stackTrace);
            throw AppException("Connection failed");
        } on FormatException {
            throw AppException("Invalid response format");
        } finally {
            client.close();
        }
    }
}

SocketException jest przechwytywane z przechwyceniem obiektu i StackTrace do szczegółowego logowania, a następnie przekazywane jako AppException. FormatException jest przechwytywane bez zmiennej — wystarczy wiedzieć, że format odpowiedzi jest nieprawidłowy. Blok finally gwarantuje zamknięcie HttpClient, zapobiegając wyciekowi gniazd. We Flutter takie podejście jest szczególnie ważne w testowaniu Widget, gdzie nieobsłużone wyjątki w State.initState prowadzą do upadku całej sesji testowej.

Typowe błędy przy używaniu Try-Catch

Nawet doświadczeni programiści popełniają błędy przy pracy z try-catch, które prowadzą do wycieku pamięci, ukrytych bugów lub nieodpowiedniego zachowania aplikacji. Rozważmy pięć najczęstszych problemów w programowaniu mobilnym.

Pusty blok catch

Pusty catch — jedna z najgorszych praktyk. Wyjątek jest połykany, aplikacja kontynuuje działanie w nieprawidłowym stanie, a programista nie dowiaduje się o problemie. Zawsze przynajmniej loguj wyjątek. W Kotlin używaj catch(e: Exception) { Log.e(...) }, w Swift — catch { print($0) }. W Dart minimalnie dopuszczalny catch powinien wywoływać debugPrint lub zapisywać do Crashlytics.

Zbyt szeroki catch

Przechwytywanie wszystkich wyjątków przez catch (Exception e) bez rozróżniania typów ukrywa nieoczekiwane błędy — NullPointerException, OutOfMemoryError, StackOverflowError. Łap tylko te typy, których się spodziewasz i które możesz obsłużyć. Dla reszty pozwól na propagację wyżej. W programowaniu mobilnym specyficzne catch dla IOException, TimeoutException, AuthException dają bardziej zrozumiałe komunikaty dla użytkownika.

Ignorowanie finally

Zasoby — pliki, gniazda, kursory BD, animacje — powinny być zwalniane w finally lub w bloku use (AutoCloseable). Programiści często zapominają zamykać zasoby przy wyjątku, co prowadzi do wycieków. W Kotlin używaj .use { } dla zasobów Closeable, w Swift — defer { }, w Dart — await using z pakietu async. Blok finally gwarantuje zwolnienie nawet przy zgłoszeniu wyjątku wewnątrz catch.

Wyjątki w wątkach i korutynach

W kodzie asynchronicznym try-catch nie przechwytuje wyjątków z innych wątków. W Kotlin Coroutines używaj CoroutineExceptionHandler lub SupervisorJob. W Swift async/await — do-catch wewnątrz Task. We Flutter — runZonedGuarded do globalnego przechwytywania. Ignorowanie tej zasady jest przyczyną trudno odtwarzalnych crashy w produkcji.

Blokowanie UI przy wyjątku

Obsługa wyjątku nie powinna blokować interfejsu użytkownika na nieokreślony czas. Pokaż użytkownikowi konkretny komunikat i daj możliwość ponowienia operacji. Snackbar z przyciskiem Retry w Kotlin/Compose, UIAlertController z action w Swift, SnackBar z action we Flutter — minimalnie wystarczający UX dla błędów sieci lub serwera. Unikaj ogólnych okien dialogowych „Wystąpił błąd" bez możliwości odzyskania.

Często zadawane pytania

Czym różni się try-catch od Result Type?

Try-catch używa wyjątków i rozwinięcia stosu do obsługi błędów, co może być kosztowne wydajnościowo przy dużej liczbie błędów. Result Type to typ-kontener (Success lub Failure), który jest obsługiwany przez pattern matching bez rozwinięcia stosu, co jest bardziej wydajne dla oczekiwanych błędów.

Czy trzeba używać finally w każdym try-catch?

Finally jest obowiązkowy, jeśli blok try otwiera zasoby (pliki, gniazda, kursory), które trzeba zamknąć. Jeśli zasoby nie są otwierane, finally nie jest wymagane. W nowoczesnych językach używaj AutoCloseable/use/defer do automatycznego zamykania zasobów bez finally. use-blok w Kotlin i Swift zastępuje finally dla obiektów Closeable.

Czy try-catch może być wolny?

W normalnym przepływie (bez wyjątku) try-catch praktycznie nie wpływa na wydajność — JVM i kompilator Swift optymalizują ten przypadek. Ale przy zgłaszaniu wyjątku następuje rozwinięcie stosu, co może zająć 10–100 µs w zależności od głębokości stosu. Nie używaj wyjątków do sterowania przepływem wykonania — to antywzorzec.

Jak obsługiwać błędy w Kotlin Coroutines?

W korutynach używaj try-catch wewnątrz coroutineScope lub CoroutineExceptionHandler do globalnego przechwytywania. SupervisorJob zapobiega anulowaniu korutyny rodzicielskiej przy błędzie w korutynie potomnej. Dla launch używaj CoroutineExceptionHandler, dla async — try-catch wokół await().

Co jest lepsze: wiele catch czy jeden catch z if-else?

Wiele catch jest preferowane: kod czyta się liniowo, każdy blok obsługuje jeden typ wyjątku. Jeden catch z if-else jest trudniejszy w utrzymaniu, łatwo przeoczyć nowy typ wyjątku. W Swift wiele catch jest obowiązkowe dla wyczerpującej obsługi enum Error, w Kotlin nie ma ograniczeń, ale najlepszą praktyką jest osobny catch na typ.

Podsumowanie

  • Try-Catch — konstrukcja przechwytywania wyjątków z blokami try, catch i opcjonalnym finally do gwarantowanego zwalniania zasobów
  • Mechanizm działania — rozwinięcie stosu przy zgłoszeniu wyjątku z poszukiwaniem odpowiedniego catch według typu błędu
  • W Swift używany jest do-catch z enum Error, try? dla opcjonalnego wyniku i try! dla gwarantowanego sukcesu
  • W Kotlin try-catch jest wyrażeniem, którego wynik można przypisać do zmiennej, wygodnie w parze z sealed class Result
  • W Dart obsługuje klauzulę on do filtrowania po typie bez zmiennej i finally do zamykania HttpClient
  • Typowe błędy: pusty catch, zbyt szerokie przechwytywanie, ignorowanie finally i brak obsługi w korutynach
  • Używaj try-catch dla nieoczekiwanych błędów, Result Type — dla oczekiwanych scenariuszy z możliwą porażką

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również