Error Propagation: nó là gì, cơ chế lan truyền lỗi và cách hoạt động trong phát triển di động

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

Error Propagation là cơ chế lan truyền lỗi lên trên ngăn xếp cuộc gọi từ nơi xảy ra đến trình xử lý. Khi một hàm không thể tự xử lý lỗi, nó sẽ chuyển lỗi cho bên gọi thông qua ngoại lệ (exception), khai báo throws hoặc Return Type. Việc triển khai propagation đúng cách rất quan trọng đối với sự ổn định của ứng dụng di động: các lỗi không được xử lý hoặc truyền sai sẽ dẫn đến sự cố. Theo Apple Swift Documentation (2026), lan truyền tự động qua throws trong Swift cho phép truyền lỗi đến bất kỳ cấp nào mà không cần mã soạn sẵn.

Những điểm chính

  • Error Propagation — truyền lỗi từ nơi xảy ra lên trên ngăn xếp đến trình xử lý, bỏ qua các hàm trung gian
  • Lan truyền tự động qua throws trong Swift truyền lỗi mà không cần mã rõ ràng ở mỗi cấp ngăn xếp
  • Lan truyền thủ công trong Kotlin và Dart yêu cầu try-catch rõ ràng hoặc truyền trong vùng chứa Result ở mỗi cấp
  • Ngoại lệ được kiểm tra trong Java buộc lan truyền qua throws trong chữ ký, ngoại lệ không kiểm tra có thể bỏ qua
  • Result Type — thay thế cho ngoại lệ, nơi lỗi được truyền như một giá trị mà không tháo ngăn xếp

Error Propagation là gì?

Error Propagation (lan truyền lỗi) là quá trình truyền đối tượng lỗi từ hàm nơi nó xảy ra lên trên chuỗi cuộc gọi đến trình xử lý phù hợp gần nhất. Hãy tưởng tượng ngăn xếp cuộc gọi: ViewController gọi ViewModel, ViewModel gọi Repository, Repository gọi API. Nếu API trả về lỗi mạng, lỗi phải đi qua Repository và ViewModel đến ViewController, nơi sẽ hiển thị thông báo cho người dùng. Mỗi hàm trung gian quyết định: xử lý lỗi hay truyền nó đi xa hơn (propagate).

Có hai cách tiếp cận để lan truyền: tự động và thủ công. Với cách tiếp cận tự động (Swift throws, ngoại lệ được kiểm tra Java), trình biên dịch buộc nhà phát triển phải xử lý lỗi hoặc khai báo lan truyền trong chữ ký. Với cách tiếp cận thủ công (Result Type, Kotlin Try), lỗi được truyền như một giá trị — nhà phát triển viết mã rõ ràng để truyền hoặc biến đổi lỗi. Theo Kotlin Result Docs (2026), Result<T> trong Kotlin không được thiết kế để lan truyền trực tiếp qua các ranh giới hàm — nó cần được biến đổi hoặc xử lý ở mỗi cấp, điều này làm cho việc lan truyền có ý thức hơn nhưng cũng dài dòng hơn.

Việc lựa chọn cách tiếp cận phụ thuộc vào kiến trúc ứng dụng và ngôn ngữ. Trong Swift, lan truyền tự động qua throws chiếm ưu thế; trong Kotlin, sử dụng hỗn hợp ngoại lệ (cho lỗi bất ngờ) và vùng chứa dạng Result (cho lỗi dự kiến). Điều quan trọng cần hiểu: lan truyền không phải là mục tiêu, mà là sự cần thiết. Một kiến trúc lý tưởng giảm thiểu độ sâu lan truyền, xử lý lỗi ở cấp thấp nhất có thể, nơi có đủ ngữ cảnh để đưa ra quyết định.

Lan truyền qua Throws trong Swift

Trong Swift, lan truyền qua throws là tự động: nếu hàm A có throws gọi hàm B có throws, và A không xử lý lỗi của B trong do-catch, lỗi sẽ tự động được truyền đến người gọi A. Điều này loại bỏ mã soạn sẵn điển hình của ngoại lệ được kiểm tra Java, nơi throws phải được khai báo trong mọi phương thức của chuỗi. Swift sử dụng nguyên tắc «một hàm throws trong chuỗi = toàn bộ chuỗi trở thành throws, nếu không được xử lý ở các cấp trung gian».

swift
struct UserRepository {
    func fetchUser(id: Int) throws -> User {
        let data = try networkService.request(path: "/users/\(id)")
        return try parseUser(from: data)
    }
}

class UserViewModel {
    let repo = UserRepository()

    func loadUser(id: Int) throws -> User {
        return try repo.fetchUser(id: id)
    }
}

// ViewController — trình xử lý cuối cùng
func onButtonTap() {
    let vm = UserViewModel()
    do {
        let user = try vm.loadUser(id: 42)
        updateUI(user)
    } catch {
        showError("Không thể tải người dùng")
    }
}

Chuỗi lan truyền: networkService.request -> fetchUser -> loadUser -> onButtonTap. Mỗi hàm trung gian được đánh dấu throws và không chứa do-catch — lỗi tự động được truyền lên trên. ViewController onButtonTap là trình xử lý cuối cùng với do-catch. Nếu ViewModel quyết định biến đổi lỗi (bọc nó trong một kiểu khác), nó có thể sử dụng do-catch và một throw mới. Lan truyền tự động giảm mã: Repository không cần biết cách xử lý lỗi — đó là trách nhiệm của ViewController, nơi có quyền truy cập vào giao diện người dùng để hiển thị thông báo cho người dùng.

Lan truyền qua ngoại lệ trong Kotlin

Trong Kotlin, lan truyền qua ngoại lệ không yêu cầu khai báo throws trong chữ ký (tất cả ngoại lệ đều không được kiểm tra). Ngoại lệ tự động đi lên ngăn xếp cho đến khi gặp try-catch. Tuy nhiên, việc thiếu throws trong chữ ký làm cho việc lan truyền trở nên ngầm định: nhà phát triển không thể thấy từ chữ ký hàm rằng nó có thể ném ra ngoại lệ. Điều này vừa là ưu điểm (ít mã soạn sẵn hơn) vừa là nhược điểm (dễ quên xử lý hơn). Kotlin giải quyết vấn đề này thông qua các quy ước và mẫu kiến trúc, chứ không phải thông qua ngôn ngữ.

kotlin
class UserRepository(
    private val api: ApiService,
    private val db: Database
) {
    suspend fun getUser(id: String): User {
        return try {
            api.fetchUser(id)
        } catch (e: IOException) {
            db.getCachedUser(id) ?: throw AppException("User unavailable")
        }
    }
}

class UserViewModel(private val repo: UserRepository) {
    private val _state = MutableStateFlow<UiState<User>>(UiState.Loading)
    val state: StateFlow<UiState<User>> = _state

    fun loadUser(id: String) {
        viewModelScope.launch {
            try {
                val user = repo.getUser(id)
                _state.value = UiState.Success(user)
            } catch (e: AppException) {
                _state.value = UiState.Error(e.message ?: "Unknown")
            }
        }
    }
}

Trong Repository, lan truyền với biến đổi: khi IOException (mạng không khả dụng), hàm cố gắng lấy dữ liệu được lưu trong bộ nhớ đệm từ cơ sở dữ liệu. Nếu bộ nhớ đệm trống, nó ném AppException — lan truyền tiếp tục với kiểu lỗi mới. ViewModel bắt AppException và chuyển đổi thành UiState.Error — lỗi không đi xa hơn, lan truyền kết thúc ở cấp lớp UI. Kotlin Coroutines thêm các đặc thù: ngoại lệ trong launch tự động lan truyền qua CoroutineExceptionHandler, còn trong async — chỉ khi gọi await(). Điều này quan trọng khi thiết kế lan truyền trong coroutine — SupervisorJob ngăn hủy coroutine cha khi có lỗi trong coroutine con.

Lan truyền qua Result Type

Một thay thế cho ngoại lệ là lan truyền thông qua kiểu vùng chứa truyền thành công hoặc lỗi như một giá trị. Trong cách tiếp cận này, hàm không trả về một giá trị mà trả về một trình bao bọc: Result<T, E> trong Swift, Result<T> trong Kotlin, Either<L, R> trong Dart (từ gói fpdart hoặc dartz). Lỗi không tháo ngăn xếp — nó chỉ nằm trong vùng chứa và cấp tiếp theo quyết định phải làm gì với nó. Điều này làm cho việc lan truyền rõ ràng và kiểm soát được hơn.

kotlin
data class HttpResult<out T>(
    val data: T?,
    val error: AppError?
) {
    val isSuccess: Boolean get() = data != null
    val isError: Boolean get() = error != null
}

sealed class AppError {
    data class Network(val message: String) : AppError()
    data class Auth(val message: String) : AppError()
}

fun fetchUser(id: String): HttpResult<User> {
    return try {
        val response = api.get("/users/$id")
        HttpResult(data = parseUser(response), error = null)
    } catch (e: IOException) {
        HttpResult(data = null, error = AppError.Network("No internet"))
    }
}

HttpResult<T> là một vùng chứa đơn giản với các trường data và error. Lớp niêm phong AppError định nghĩa các kiểu lỗi (Network, Auth). Hàm fetchUser trả về HttpResult, lan truyền không yêu cầu tháo ngăn xếp — bên gọi chỉ cần kiểm tra isSuccess/isError. Cách tiếp cận này đặc biệt hữu ích trong Clean Architecture, nơi mỗi lớp (data, domain, presentation) có thể biến đổi lỗi: IOError -> DomainError -> UiError. Lan truyền qua vùng chứa làm cho các biến đổi này rõ ràng và có thể kiểm tra được, không giống như ngoại lệ, nơi chuỗi biến đổi không thể nhìn thấy trong chữ ký hàm.

Lan truyền so với Xử lý: khi nào truyền, khi nào xử lý

Một trong những quyết định quan trọng trong thiết kế xử lý lỗi là lựa chọn giữa lan truyền (truyền lên trên) và xử lý (xử lý tại đây). Quy tắc quyết định: xử lý lỗi ở cấp có đủ ngữ cảnh cho một hành động có ý nghĩa. Nếu bạn có quyền truy cập vào giao diện người dùng — hiển thị thông báo cho người dùng. Nếu bạn có quyền truy cập vào bộ nhớ đệm — hãy thử khôi phục. Nếu bạn không có cả hai — hãy lan truyền.

Tình huốngHành độngLý do
Lỗi mạng trong RepositoryLan truyềnRepository không biết người dùng có muốn thử lại yêu cầu không
Lỗi phân tích trong RepositoryXử lý (trả về mặc định)Repository biết định dạng, có thể trả về giá trị dự phòng
Hết thời gian trong ViewModelXử lý (UiState.Error)ViewModel quản lý UiState, biết cách chuyển đổi lỗi
Lỗi ủy quyền trong InterceptorXử lý (làm mới mã thông báo)Interceptor có quyền truy cập vào mã thông báo và có thể khôi phục phiên
Lỗi không xác định trong UseCaseLan truyềnUseCase không có ngữ cảnh giao diện người dùng — chỉ logic nghiệp vụ

Nguyên tắc vàng: lan truyền tối thiểu, xử lý tối đa ở các cấp thấp hơn. Nếu Repository có thể khôi phục từ bộ nhớ đệm — nó nên làm như vậy mà không truyền lỗi lên trên. Nếu ViewModel có thể hiển thị Snackbar — hãy để nó hiển thị, mà không yêu cầu thêm mã từ ViewController. Mỗi cấp lan truyền làm tăng sự kết nối và làm phức tạp việc kiểm thử. Theo Google Android Architecture Guide (2026), khuyến nghị giảm thiểu lan truyền qua ranh giới lớp bằng cách sử dụng lớp niêm phong UiState để đại diện cho tất cả các trạng thái có thể (Loading, Success, Error) ở cấp ViewModel và không truyền ngoại lệ trực tiếp đến lớp UI.

Vấn đề và phản mẫu của Error Propagation

Lan truyền không chính xác là nguồn gốc của các lỗi khó tìm trong ứng dụng di động. Hãy xem xét năm vấn đề chính mà các nhà phát triển gặp phải và cách giải quyết chúng.

Mất ngữ cảnh lỗi

Vấn đề phổ biến nhất: trong quá trình lan truyền, một ngoại lệ bị bắt, được ghi nhật ký và một ngoại lệ mới được ném ra mà không có ngoại lệ gốc. Nhà phát triển mất StackTrace và không thể hiểu lỗi xảy ra chính xác ở đâu. Trong Swift, hãy sử dụng chuỗi lỗi: throw MyError(context: originalError). Trong Kotlin: throw AppException(cause = originalException). Trong Dart: throw AppException(message, originalException). Không bao giờ tạo ngoại lệ mới mà không truyền cause/underlyingError.

Bỏ qua lỗi (catch rỗng)

catch (e: Exception) { /* không làm gì */ } là một phản mẫu khiến ứng dụng tiếp tục hoạt động ở trạng thái không chính xác. Nếu bạn chắc chắn rằng lỗi có thể bỏ qua — hãy thêm bình luận với lý do. Trong Swift, hãy sử dụng try? để bỏ qua tùy chọn (lỗi -> nil). Trong Kotlin — Result<T>.onFailure { /* log */ }. Đừng nuốt ngoại lệ mà không ghi nhật ký.

Độ sâu lan truyền quá mức

Nếu lỗi đi qua 5+ cấp mà không được xử lý, kiến trúc cần được xem xét lại. Mỗi cấp lan truyền là sự phụ thuộc vào chữ ký throws của các hàm bên dưới. Giải pháp: sử dụng vùng chứa Thất bại (sealed class Result { Success, Error }) ở ranh giới lớp để làm cho việc lan truyền rõ ràng và có giới hạn. Chuỗi lan truyền càng ngắn, việc kiểm thử và gỡ lỗi mã càng dễ dàng.

Lan truyền trong coroutine không có SupervisorJob

Trong Kotlin Coroutines, ngoại lệ trong launch theo mặc định hủy coroutine cha và tất cả siblings (con của cùng phạm vi). Nếu một trong 10 tác vụ song song thất bại, 9 tác vụ còn lại sẽ bị hủy, điều này thường không mong muốn. Sử dụng SupervisorJob hoặc supervisorScope để cách ly lỗi: lỗi trong một con không hủy siblings. ViewModelScope sử dụng SupervisorJob theo mặc định, giúp tránh vấn đề này trong Android.

Lan truyền qua callback không xử lý

Trong các API dựa trên callback, lỗi thường được truyền như một tham số của callback. Nếu callback không xử lý lỗi (hoặc xử lý không chính xác), việc lan truyền trở nên ngầm định và dễ bị mất. Giải pháp: di chuyển sang async/await (Swift) hoặc coroutine (Kotlin), nơi lan truyền hoạt động thông qua cơ chế try-catch tiêu chuẩn. Nếu callback là không thể tránh khỏi — hãy sử dụng Either<Error, T> hoặc Result<T> để buộc xử lý cả hai trường hợp.

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

Error Propagation khác throw như thế nào?

Throw là hành động một lần để ném ngoại lệ. Error Propagation là toàn bộ quá trình truyền lỗi qua nhiều cấp ngăn xếp, từ throw đến catch. Lan truyền bao gồm throw, truyền tự động hoặc thủ công qua các hàm trung gian và xử lý cuối cùng. Đây là một khái niệm rộng hơn mô tả vòng đời của lỗi.

Làm thế nào để kiểm tra Error Propagation?

Sử dụng đối tượng mock ném ngoại lệ trong các kịch bản nhất định. Kiểm tra rằng hàm lan truyền hoặc xử lý lỗi chính xác qua assertThrows (Kotlin/JUnit) hoặc XCTAssertThrowsError (Swift/XCTest). Đối với lan truyền dựa trên Result, kiểm tra isSuccess/isError và các giá trị trong cả hai trường hợp.

Khi nào lan truyền qua Result tốt hơn ngoại lệ?

Lan truyền qua Result được ưu tiên cho lỗi dự kiến (dữ liệu không hợp lệ, quy tắc nghiệp vụ) trong một ranh giới kiến trúc. Ngoại lệ tốt hơn cho lỗi bất ngờ (mất mạng, lỗi I/O) cần được xử lý ở cấp cao. Kết quả có lỗi không làm gián đoạn luồng thực thi, ngoại lệ thì có.

Làm thế nào để lan truyền lỗi qua coroutine Kotlin?

Trong Kotlin Coroutines, ngoại lệ trong launch tự động lan truyền qua CoroutineScope với việc hủy siblings. Sử dụng supervisorScope hoặc SupervisorJob để cách ly: lỗi trong một coroutine không hủy các coroutine khác. Đối với async, lỗi phải được xử lý rõ ràng qua try-catch khi gọi await(), nếu không nó sẽ bị nuốt.

Cấp nào nên là trình xử lý lỗi cuối cùng?

Trình xử lý cuối cùng lý tưởng là lớp UI (ViewController, Fragment/Composable). Chỉ nó mới có quyền truy cập vào giao diện người dùng và có thể hiển thị thông báo, Snackbar hoặc hộp thoại. Các lớp trung gian (Repository, UseCase, ViewModel) lan truyền lỗi, biến đổi nó nếu cần thành kiểu miền trừu tượng hơn.

Tổng kết

  • Error Propagation — truyền lỗi lên trên ngăn xếp từ nơi xảy ra đến trình xử lý qua ngoại lệ hoặc vùng chứa Result
  • Lan truyền tự động trong Swift qua throws không yêu cầu mã ở các cấp trung gian — lỗi tự động đi lên
  • Lan truyền thủ công trong Kotlin qua try-catch rõ ràng và throw ở mỗi cấp làm cho việc xử lý có ý thức nhưng dài dòng
  • Result Type truyền lỗi như một giá trị mà không tháo ngăn xếp, thuận tiện cho lỗi dự kiến trong Clean Architecture
  • Quy tắc xử lý: xử lý ở cấp có ngữ cảnh (UI), lan truyền qua các cấp không có ngữ cảnh (domain, data)
  • Phản mẫu: catch rỗng, mất cause khi biến đổi, độ sâu lan truyền quá mức, bỏ qua SupervisorJob
  • Thiết kế lỗi được phân loại (sealed class / enum) cho mỗi lớp và biến đổi chúng khi vượt qua ranh giới lớp

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