Try-Catch: bản chất, cấu trúc bắt ngoại lệ và cách hoạt động trong phát triển di động

Tác giả: IT Sectr Đã đăng: 2026-05-25 Thời gian đọc: 9 phút

Try-Catch là cấu trúc bắt ngoại lệ cho phép thực thi mã tiềm ẩn nguy hiểm trong một khối được bảo vệ và xử lý lỗi một cách chính xác mà không làm chương trình bị treo. Khối try chứa mã có thể ném ngoại lệ, catch bắt nó và thực thi logic khôi phục. Theo Apple Swift Documentation (2026), khối finally được thực thi bất kể ngoại lệ có được ném ra hay không, đảm bảo giải phóng tài nguyên.

Những điểm chính

  • Try-Catch là cấu trúc bắt ngoại lệ ngăn chặn việc chương trình bị treo khi có lỗi
  • Khối try chứa mã có thể ném ngoại lệ — việc thực thi bị dừng lại ở lỗi đầu tiên
  • Khối catch bắt ngoại lệ của loại được chỉ định và thực thi logic xử lý hoặc khôi phục
  • Khối finally được thực thi đảm bảo sau try/catch để giải phóng tài nguyên, ví dụ đóng tệp
  • Try-catch lồng nhau cho phép xử lý lỗi ở các mức trừu tượng khác nhau trong cùng một hàm

Try-Catch là gì?

Try-Catch là một cấu trúc xử lý ngoại lệ có cấu trúc cơ bản có mặt trong hầu hết các ngôn ngữ lập trình hiện đại. Nó bao gồm ba khối: try (cố gắng thực thi mã nguy hiểm), catch (bắt và xử lý ngoại lệ) và finally tùy chọn (kết thúc). Ý tưởng của cấu trúc này là tách biệt logic kinh doanh khỏi logic xử lý lỗi, làm cho mã dễ đọc và dễ dự đoán hơn.

Khái niệm này lần đầu tiên được triển khai trong C++ dưới dạng try/catch, sau đó được Java, C#, Swift, Kotlin, Dart, Python, JavaScript và các ngôn ngữ khác áp dụng. Mỗi ngôn ngữ thêm các tính năng riêng: trong Swift, khối catch phải đầy đủ, trong Kotlin, try-catch có thể là một biểu thức, trong Dart, finally là bắt buộc đối với tài nguyên luồng. Bất chấp sự khác biệt, nguyên tắc cơ bản là giống nhau: lỗi được xử lý càng gần nơi xảy ra càng tốt, chứ không phải trên toàn cục.

Việc sử dụng Try-Catch đặc biệt quan trọng trong phát triển di động, nơi các yếu tố bên ngoài — mất mạng, phản hồi máy chủ không chính xác, bộ nhớ không đủ — xảy ra liên tục. Xử lý ngoại lệ đúng cách ngăn chặn sự cố ứng dụng và đảm bảo UX chính xác: người dùng thấy thông báo lỗi thay vì ứng dụng đột ngột đóng lại. Theo Google Android Kotlin Style Guide (2026), mọi hàm có thể ném ngoại lệ phải xử lý nó qua try-catch hoặc khai báo throws trong chữ ký của nó.

Try-Catch hoạt động như thế nào

Cơ chế thực thi try-catch dựa trên việc mở xếp chồng (stack unwinding). Khi một ngoại lệ được ném ra bên trong khối try thông qua toán tử throw (hoặc do lỗi hệ thống), luồng thực thi bình thường bị gián đoạn ngay lập tức. Việc thực thi di chuyển lên một cấp trong ngăn xếp cuộc gọi để tìm kiếm khối catch phù hợp. Các ngôn ngữ hiện đại tìm kiếm catch có loại khớp với loại ngoại lệ được ném ra bằng cách sử dụng cơ chế so khớp loại (type matching).

Nếu tìm thấy catch phù hợp, phần thân của nó được thực thi, sau đó việc thực thi tiếp tục sau toàn bộ cấu trúc try-catch-finally. Nếu không tìm thấy catch nào, ngoại lệ đi lên cao hơn trong ngăn xếp và có thể được xử lý ở cấp cao hơn — cho đến một trình xử lý toàn cục, trong ứng dụng di động sẽ hiển thị hộp thoại lỗi cho người dùng. Nếu ngoại lệ không được xử lý ở bất kỳ đâu, ứng dụng sẽ bị treo. Đây là lý do tại sao việc xử lý chính xác tất cả các loại ngoại lệ có thể xảy ra là rất quan trọng đối với sự ổn định của ứng dụng.

Cú pháp cấu trúc cơ bản

kotlin
fun readUserData(): User {
    return try {
        val response = api.fetchUser()
        parseUser(response)
    } catch (e: IOException) {
        logError("Lỗi mạng", e)
        throw AppException("Không thể tải dữ liệu")
    } catch (e: JsonParseException) {
        logError("Lỗi phân tích", e)
        return User.default()
    } finally {
        closeLoadingIndicator()
    }
}

Mã đầu tiên cố gắng thực hiện yêu cầu API và phân tích phản hồi. Nếu xảy ra IOException (vấn đề mạng), ngoại lệ được ghi lại và ném lại dưới dạng AppException. Nếu xảy ra JsonParseException — người dùng mặc định được trả về. Khối finally đảm bảo ẩn chỉ báo tải, ngăn chặn rò rỉ thành phần UI trên màn hình.

Try-Catch trong Swift

Trong Swift, xử lý lỗi được triển khai thông qua giao thức Error (trước đây là ErrorType). Bất kỳ loại nào tuân thủ Error đều có thể được ném ra thông qua toán tử throw. Một hàm có thể ném lỗi được đánh dấu bằng từ khóa throws trong chữ ký của nó. Việc gọi một hàm như vậy yêu cầu tiền tố try (cho try-catch rõ ràng), try? (kết quả tùy chọn) hoặc try! (thực thi bắt buộc không xử lý lỗi).

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("Mạng " + error.localizedDescription)
} catch {
    showGenericError()
}

Enum NetworkError triển khai giao thức Error, định nghĩa ba trường hợp: noConnection, serverError với mã và timeout. Hàm fetchUser được đánh dấu throws: đầu tiên kiểm tra kết nối, sau đó thực hiện yêu cầu và phân tích. Trong khối do-catch, ba catch xử lý các tình huống khác nhau: trường hợp cụ thể noConnection, loại NetworkError chung và tất cả các lỗi khác. Điều này cho phép hiển thị các thông báo khác nhau cho người dùng tùy theo loại vấn đề.

Try-Catch trong Kotlin

Kotlin kế thừa try-catch-finally từ Java nhưng thêm một điểm khác biệt quan trọng: trong Kotlin, try-catch là một biểu thức (expression), không phải là câu lệnh (statement). Điều này có nghĩa là kết quả của khối try hoặc khối catch có thể được gán cho một biến. Biểu thức cuối cùng trong khối try trở thành kết quả khi thành công, biểu thức cuối cùng trong catch — khi lỗi. Nếu lỗi không được xử lý bởi bất kỳ catch nào, ngoại lệ đi lên cao hơn trong ngăn xếp.

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("Mạng ", e)
        Result.Error(e)
    }
}

Trong ví dụ, sealed class Result bao bọc một phản hồi thành công hoặc một lỗi. Hàm loadData sử dụng try-catch như một biểu thức: khi thành công trả về Result.Success, khi HttpException hoặc IOException — Result.Error với ghi log. Cách tiếp cận này cho phép người gọi xử lý lỗi mà không cần ngoại lệ — thông qua biểu thức when trên loại Result. Điều này đặc biệt thuận tiện trong Jetpack Compose để hiển thị các trạng thái UI khác nhau (Loading, Success, Error) thông qua StateFlow và collectAsState.

Try-Catch trong Dart và Flutter

Dart hỗ trợ try-catch-finally với cú pháp tương tự Java, nhưng có thêm mệnh đề on để lọc theo loại ngoại lệ mà không cần chỉ định biến. Điều này thuận tiện khi bản thân ngoại lệ không cần thiết — chỉ loại của nó mới quan trọng. Dart cũng hỗ trợ khối catch với hai tham số: đối tượng ngoại lệ và StackTrace, hữu ích cho việc ghi lại toàn bộ chuỗi cuộc gọi.

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 được bắt cùng với đối tượng ngoại lệ và StackTrace để ghi log chi tiết, sau đó được ném lại dưới dạng AppException. FormatException được bắt mà không có biến — biết rằng định dạng phản hồi không chính xác là đủ. Khối finally đảm bảo đóng HttpClient, ngăn chặn rò rỉ socket. Trong Flutter, cách tiếp cận này đặc biệt quan trọng đối với kiểm thử Widget, nơi các ngoại lệ không được xử lý trong State.initState gây ra sự cố toàn bộ phiên kiểm thử.

Những lỗi thường gặp khi sử dụng Try-Catch

Ngay cả các lập trình viên giàu kinh nghiệm cũng mắc lỗi khi làm việc với try-catch dẫn đến rò rỉ bộ nhớ, lỗi ẩn hoặc hành vi ứng dụng không phù hợp. Hãy xem xét năm vấn đề phổ biến nhất trong phát triển di động.

Khối catch trống

Catch trống là một trong những thực hành tồi tệ nhất. Ngoại lệ bị nuốt, ứng dụng tiếp tục hoạt động ở trạng thái không chính xác và lập trình viên không biết về vấn đề. Luôn ít nhất ghi lại ngoại lệ. Trong Kotlin, sử dụng catch(e: Exception) { Log.e(...) }, trong Swift — catch { print($0) }. Trong Dart, catch tối thiểu chấp nhận được phải gọi debugPrint hoặc ghi vào Crashlytics.

Catch quá rộng

Bắt tất cả các ngoại lệ qua catch (Exception e) mà không phân biệt theo loại sẽ che giấu các lỗi không mong đợi — NullPointerException, OutOfMemoryError, StackOverflowError. Chỉ bắt những loại bạn mong đợi và có thể xử lý. Đối với mọi thứ khác, cho phép lan truyền lên trên. Trong phát triển di động, catch cụ thể cho IOException, TimeoutException, AuthException cung cấp thông báo có ý nghĩa hơn cho người dùng.

Bỏ qua finally

Tài nguyên — tệp, socket, con trỏ CSDL, hoạt ảnh — phải được giải phóng trong finally hoặc trong khối use (AutoCloseable). Các lập trình viên thường quên đóng tài nguyên khi có ngoại lệ, dẫn đến rò rỉ. Trong Kotlin, sử dụng .use { } cho tài nguyên Closeable, trong Swift — defer { }, trong Dart — await using từ gói async. Khối finally đảm bảo giải phóng ngay cả khi ngoại lệ được ném ra bên trong catch.

Ngoại lệ trong luồng và coroutine

Trong mã bất đồng bộ, try-catch không bắt được ngoại lệ từ các luồng khác. Trong Kotlin Coroutines, sử dụng CoroutineExceptionHandler hoặc SupervisorJob. Trong Swift async/await — do-catch bên trong Task. Trong Flutter — runZonedGuarded để bắt toàn cục. Bỏ qua quy tắc này là nguyên nhân gây ra các sự cố khó tái tạo trong sản xuất.

Chặn UI khi có ngoại lệ

Xử lý ngoại lệ không nên chặn giao diện người dùng vô thời hạn. Hiển thị cho người dùng một thông báo cụ thể và cho họ cơ hội thử lại thao tác. Snackbar với nút Retry trong Kotlin/Compose, UIAlertController với hành động trong Swift, SnackBar với hành động trong Flutter — UX tối thiểu đủ cho lỗi mạng hoặc máy chủ. Tránh các hộp thoại chung chung “Đã xảy ra lỗi” mà không có tùy chọn khôi phục.

Các câu hỏi thường gặp

Try-catch khác Result Type như thế nào?

Try-catch sử dụng ngoại lệ và mở xếp chồng để xử lý lỗi, điều này có thể tốn kém về hiệu suất khi có nhiều lỗi. Result Type là loại vùng chứa (Success hoặc Failure) được xử lý thông qua so khớp mẫu mà không cần mở xếp chồng, hiệu quả hơn cho các lỗi dự kiến.

Có cần sử dụng finally trong mọi try-catch không?

Finally là bắt buộc nếu khối try mở tài nguyên (tệp, socket, con trỏ) cần được đóng. Nếu không mở tài nguyên, finally không cần thiết. Trong các ngôn ngữ hiện đại, sử dụng AutoCloseable/use/defer để tự động đóng tài nguyên mà không cần finally. Khối use trong Kotlin và Swift thay thế finally cho các đối tượng Closeable.

Try-catch có thể chậm không?

Trong luồng bình thường (không có ngoại lệ), try-catch hầu như không ảnh hưởng đến hiệu suất — JVM và trình biên dịch Swift tối ưu hóa trường hợp này. Nhưng khi một ngoại lệ được ném ra, việc mở xếp chồng xảy ra, có thể mất 10–100 μs tùy thuộc vào độ sâu của ngăn xếp. Không sử dụng ngoại lệ để điều khiển luồng — đây là phản mẫu.

Làm thế nào để xử lý lỗi trong Kotlin Coroutines?

Trong coroutine, sử dụng try-catch bên trong coroutineScope hoặc CoroutineExceptionHandler để bắt toàn cục. SupervisorJob ngăn chặn việc hủy coroutine cha khi coroutine con thất bại. Đối với launch, sử dụng CoroutineExceptionHandler, đối với async — try-catch xung quanh await().

Nên dùng nhiều catch hay một catch với if-else?

Nhiều catch được ưu tiên hơn: mã được đọc tuyến tính, mỗi khối xử lý một loại ngoại lệ. Một catch với if-else khó bảo trì hơn và dễ bỏ sót loại ngoại lệ mới. Trong Swift, nhiều catch là bắt buộc để xử lý đầy đủ enum Error, trong Kotlin không có hạn chế, nhưng thực hành tốt nhất là catch riêng cho từng loại.

Tổng kết

  • Try-Catch là cấu trúc bắt ngoại lệ với các khối try, catch và finally tùy chọn để giải phóng tài nguyên đảm bảo
  • Cơ chế hoạt động — mở xếp chồng khi ném ngoại lệ và tìm kiếm catch phù hợp theo loại lỗi
  • Trong Swift sử dụng do-catch với enum Error, try? cho kết quả tùy chọn và try! cho thành công đảm bảo
  • Trong Kotlin try-catch là biểu thức có kết quả gán được cho biến, thuận tiện với sealed class Result
  • Trong Dart hỗ trợ mệnh đề on để lọc theo loại không cần biến và finally để đóng HttpClient
  • Lỗi thường gặp: catch trống, catch quá rộng, bỏ qua finally và thiếu xử lý trong coroutine
  • Sử dụng try-catch cho lỗi không mong đợi, Result Type — cho các tình huống dự kiến có thể thất bại

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm