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 — 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.
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.
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.
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).
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.
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.
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.
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ń.
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.
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 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.
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.
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.
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.
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
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.
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.
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.
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().
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
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.
Przeczytaj również