Try-Catch is an exception handling construct that allows executing potentially dangerous code in a protected block and correctly handling errors without crashing the program. The try block contains code that may throw an exception, catch intercepts it and executes recovery logic. According to Apple Swift Documentation (2026), the finally block executes regardless of whether an exception was thrown or not, guaranteeing resource release.
Key Takeaways
Try-Catch is a fundamental structural exception handling construct present in most modern programming languages. It consists of three blocks: try (attempting to execute dangerous code), catch (intercepting and handling the exception), and optional finally (finalization). The idea of the construct is to separate business logic from error handling logic, making code more readable and predictable.
The concept was first implemented in C++ as try/catch, then adopted by Java, C#, Swift, Kotlin, Dart, Python, JavaScript, and other languages. Each language adds its own features: in Swift the catch block must be exhaustive, in Kotlin try-catch can be an expression, in Dart finally is required for stream resources. Despite the differences, the basic principle is the same: an error is handled as close to the point of occurrence as possible, not globally.
Using Try-Catch is especially important in mobile development, where external factors — network loss, incorrect server response, insufficient memory — occur constantly. Proper exception handling prevents app crashes and ensures correct UX: the user sees an error message instead of a sudden app closure. According to Google Android Kotlin Style Guide (2026), every function that may throw an exception should either handle it via try-catch or declare throws in its signature.
The try-catch execution mechanism is based on stack unwinding. When an exception is thrown inside the try block via the throw operator (or as a result of a system error), the normal execution flow is immediately interrupted. Execution moves up one level in the call stack searching for a suitable catch block. Modern languages look for a catch with a type matching the thrown exception type using a type matching mechanism.
If a matching catch is found, its body executes, after which execution continues after the entire try-catch-finally construct. If no catch is found, the exception propagates further up the stack and may be handled at a higher level — up to a global handler, which in a mobile app shows the user an error dialog. If the exception is not handled anywhere, the application crashes. This is why correct handling of all possible exception types is critical for application stability.
fun readUserData(): User {
return try {
val response = api.fetchUser()
parseUser(response)
} catch (e: IOException) {
logError("Network error", e)
throw AppException("Failed to load data")
} catch (e: JsonParseException) {
logError("Parse error", e)
return User.default()
} finally {
closeLoadingIndicator()
}
}
The code first attempts to make an API request and parse the response. If an IOException (network problem) occurs, the exception is logged and rethrown as AppException. If a JsonParseException occurs — a default user is returned. The finally block guarantees hiding the loading indicator, preventing UI component leaks on the screen.
In Swift, error handling is implemented through the Error protocol (formerly ErrorType). Any type conforming to Error can be thrown using the throw operator. A function that can throw an error is marked with the throws keyword in its signature. Calling such a function requires the try prefix (for explicit try-catch), try? (optional result), or try! (forced execution without error handling).
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("Network " + error.localizedDescription)
} catch {
showGenericError()
}
The enum NetworkError implements the Error protocol, defining three cases: noConnection, serverError with a code, and timeout. The fetchUser function is marked throws: it first checks the connection, then performs the request and parsing. In the do-catch block, three catch clauses handle different scenarios: the specific noConnection case, the general NetworkError type, and all other errors. This allows showing the user different messages depending on the problem type.
Kotlin inherited try-catch-finally from Java but added an important difference: in Kotlin, try-catch is an expression, not a statement. This means the result of a try block or catch block can be assigned to a variable. The last expression in the try block becomes the result on success, the last expression in the catch block — on error. If the error is not handled by any catch, the exception propagates up the stack.
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("Network ", e)
Result.Error(e)
}
}
In the example, sealed class Result wraps a successful response or an error. The loadData function uses try-catch as an expression: on success it returns Result.Success, on HttpException or IOException — Result.Error with logging. This approach allows the caller to handle errors without exceptions — via a when expression on the Result type. This is especially convenient in Jetpack Compose for displaying different UI states (Loading, Success, Error) through StateFlow and collectAsState.
Dart supports try-catch-finally with syntax similar to Java, but with the addition of an on-clause for filtering by exception type without specifying a variable. This is convenient when the exception itself is not needed — only the fact of its type matters. Dart also supports a catch block with two parameters: the exception object and StackTrace, which is useful for logging the full call chain.
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 is caught with both the exception object and StackTrace for detailed logging, then rethrown as AppException. FormatException is caught without a variable — knowing the response format is incorrect is enough. The finally block guarantees closing the HttpClient, preventing socket leaks. In Flutter, this approach is especially important for Widget testing, where unhandled exceptions in State.initState cause the entire test session to crash.
Even experienced developers make mistakes when working with try-catch that lead to memory leaks, hidden bugs, or inappropriate application behavior. Let's look at the five most common problems in mobile development.
Empty catch is one of the worst practices. The exception is swallowed, the application continues operating in an incorrect state, and the developer never learns about the problem. Always at least log the exception. In Kotlin use catch(e: Exception) { Log.e(...) }, in Swift — catch { print($0) }. In Dart, a minimally acceptable catch should call debugPrint or write to Crashlytics.
Catching all exceptions via catch (Exception e) without type distinction hides unexpected errors — NullPointerException, OutOfMemoryError, StackOverflowError. Catch only the types you expect and can handle. For everything else, allow propagation upward. In mobile development, specific catch blocks for IOException, TimeoutException, AuthException give more meaningful messages to the user.
Resources — files, sockets, database cursors, animations — must be released in finally or in a use-block (AutoCloseable). Developers often forget to close resources when an exception occurs, leading to leaks. In Kotlin use .use { } for Closeable resources, in Swift — defer { }, in Dart — await using from the async package. The finally block guarantees release even when an exception is thrown inside catch.
In asynchronous code, try-catch does not catch exceptions from other threads. In Kotlin Coroutines use CoroutineExceptionHandler or SupervisorJob. In Swift async/await — do-catch inside Task. In Flutter — runZonedGuarded for global interception. Ignoring this rule is the cause of hard-to-reproduce crashes in production.
Exception handling should not block the user interface indefinitely. Show the user a specific message and give them the opportunity to retry the operation. Snackbar with a Retry button in Kotlin/Compose, UIAlertController with action in Swift, SnackBar with action in Flutter — a minimally sufficient UX for network or server errors. Avoid generic “An error occurred” dialogs without recovery options.
Frequently Asked Questions
Try-catch uses exceptions and stack unwinding for error handling, which can be costly in performance when dealing with many errors. Result Type is a container type (Success or Failure) that is handled via pattern matching without stack unwinding, making it more efficient for expected errors.
Finally is required if the try block opens resources (files, sockets, cursors) that need to be closed. If no resources are opened, finally is not needed. In modern languages, use AutoCloseable/use/defer for automatic resource closing without finally. The use-block in Kotlin and Swift replaces finally for Closeable objects.
In the normal flow (without exceptions), try-catch has practically no performance impact — the JVM and Swift compiler optimize this case. However, when an exception is thrown, stack unwinding occurs, which can take 10–100 μs depending on stack depth. Do not use exceptions for flow control — this is an anti-pattern.
In coroutines, use try-catch inside coroutineScope or CoroutineExceptionHandler for global interception. SupervisorJob prevents the parent coroutine from being cancelled when a child coroutine fails. For launch use CoroutineExceptionHandler, for async — try-catch around await().
Multiple catches are preferable: the code reads linearly, each block handles one exception type. A single catch with if-else is harder to maintain, and it’s easy to miss a new exception type. In Swift, multiple catches are mandatory for exhaustive enum Error handling, in Kotlin there are no restrictions, but best practice is a separate catch per type.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also