Typed Throws trong Swift 6.2: Hướng Dẫn Toàn Diện Về Error Handling Type-Safe Cho iOS Developer

Khám phá typed throws trong Swift 6.2 — error handling type-safe đã thay đổi cách iOS developer xử lý lỗi. Hướng dẫn chi tiết cú pháp throws(ErrorType), so sánh với Result type, mô hình Catching protocol giải quyết nesting hell, kèm best practices thực tế năm 2026.

Typed Throws Swift 6.2: Complete Guide 2026

Thật lòng mà nói, tôi đã chờ tính năng này từ ngày Swift 2.0 ra mắt từ khóa throws. Sau hơn một thập kỷ, cộng đồng Swift cuối cùng cũng có được thứ mà nhiều developer mong mỏi: typed throws. Được giới thiệu chính thức qua đề xuất SE-0413 trong Swift 6.0 và hoàn thiện ở Swift 6.2, tính năng này cho phép bạn chỉ định chính xác loại lỗi nào mà một hàm có thể ném ra. Đây không chỉ là cải tiến cú pháp đâu — nó là bước tiến thực sự về type safety, hiệu năngtính rõ ràng của API.

Trong bài hướng dẫn này, bạn sẽ học cách dùng typed throws hiệu quả, giải quyết vấn đề "nesting hell" khi lỗi lan truyền qua nhiều tầng, so sánh với Result type, và áp dụng các best practices vào dự án iOS năm 2026. Bắt đầu thôi.

Typed Throws là gì và Tại sao Quan trọng?

Trước Swift 6, từ khóa throws không cho phép bạn chỉ định loại lỗi cụ thể. Một hàm khai báo throws có thể ném ra bất kỳ kiểu nào tuân thủ giao thức Error. Nghe quen không? Hệ quả thì khá đau đầu:

  • Người gọi không có bảo đảm thời điểm biên dịch về các loại lỗi có thể xảy ra
  • Khối catch phải xử lý any Error, thường yêu cầu ép kiểu thủ công
  • Compiler không thể giúp bạn xử lý đầy đủ tất cả các trường hợp lỗi
  • Trong code hiệu năng cao, mỗi lỗi đều phải đi qua existential container, gây overhead bộ nhớ

Với typed throws trong Swift 6.2, mọi thứ trở nên rõ ràng hơn rất nhiều:

enum NetworkError: Error {
    case timeout
    case noConnection
    case invalidResponse(statusCode: Int)
}

func fetchData(from url: URL) throws(NetworkError) -> Data {
    // Compiler đảm bảo chỉ NetworkError có thể được ném ra
    throw NetworkError.timeout
}

Khi gọi hàm này, Swift tự động suy luận kiểu lỗi trong khối catch mà không cần ép kiểu (đây là điểm tôi thích nhất):

do {
    let data = try fetchData(from: url)
    process(data)
} catch {
    // 'error' đã có kiểu NetworkError, không phải any Error!
    switch error {
    case .timeout:
        showTimeoutAlert()
    case .noConnection:
        showOfflineMode()
    case .invalidResponse(let code):
        log("HTTP \(code)")
    }
}

Ba Trạng Thái Đặc Biệt của throws

Swift 6.2 thống nhất ba dạng khai báo throws thành một mô hình duy nhất:

  • throws — tương đương với throws(any Error) (kiểu cũ, không type-safe)
  • throws(ErrorType) — typed throws mới, ném đúng một kiểu lỗi cụ thể
  • throws(Never) — tương đương hàm không ném lỗi (non-throwing)

Nói cách khác, toàn bộ hệ thống error handling của Swift hiện đã thống nhất, và compiler có thể áp dụng các tối ưu hóa mạnh hơn cho từng trường hợp. Đẹp.

So Sánh Typed Throws với Result Type

Trước khi có typed throws, nhiều developer Swift (bao gồm cả tôi) đã chuyển sang dùng Result<Success, Failure: Error> để có được tính chất type-safe. Nhưng mỗi cách có ưu nhược điểm riêng — và sự thật là cả hai vẫn có chỗ đứng trong codebase hiện đại.

Bảng So Sánh Chi Tiết

Tiêu chíthrows (untyped)throws(ErrorType)Result<T, E>
Type safety lỗiKhông
Trải nghiệm do-catchTốtXuất sắcCần map/flatMap
Tích hợp async/awaitCần thủ công
Có thể lưu trữ làm valueKhôngKhông
Hiệu năng (zero-cost)Không
Phù hợp Public APIKhuyến nghịCẩn trọngTốt

Khi nào nên dùng Typed Throws?

Theo khuyến nghị chính thức từ đề xuất SE-0413, typed throws phù hợp nhất trong các trường hợp sau:

  1. Code nội bộ trong một module hoặc package — nơi bạn luôn muốn xử lý hết lỗi và lỗi là chi tiết triển khai
  2. Code generic không tự sinh ra lỗi — chỉ truyền tiếp lỗi từ closure do người dùng cung cấp (ví dụ: map, filter)
  3. Code dependency-free trong môi trường ràng buộc — như Embedded Swift, nơi không thể cấp phát bộ nhớ động

Khi nào nên giữ Result?

Result vẫn là lựa chọn tốt khi bạn cần:

  • Lưu trữ kết quả thành công/thất bại như một property hoặc truyền qua hàm
  • Làm việc với API callback-based hoặc Combine
  • Biểu diễn kết quả deferred mà không dùng async/await

Khi nào nên giữ throws untyped?

Đây là điểm mà nhiều developer hay bỏ qua: throws không có kiểu vẫn là mặc định cho hầu hết public API. Lý do? Bạn thường muốn linh hoạt thêm các loại lỗi mới mà không phá vỡ ABI hoặc API. Cố định kiểu lỗi quá sớm — và một thay đổi nhỏ trong tương lai có thể trở thành breaking change. Tin tôi đi, tôi đã từng dính phải tình huống này khi maintain một SDK nội bộ.

Hiệu Năng: Tại sao Typed Throws Nhanh hơn?

Một trong những lý do quan trọng để dùng typed throws nằm ở hiệu năng. Hãy xem cơ chế bộ nhớ:

// Untyped throws — yêu cầu type erasure
func fetchUntyped() throws -> Data { ... }
// Bộ nhớ: [Existential Container] -> [Type Metadata] -> [Error Value]

// Typed throws — không cần type erasure
func fetchTyped() throws(NetworkError) -> Data { ... }
// Bộ nhớ: [Error Value] (trực tiếp)

Với throws không có kiểu, mỗi lỗi được "đóng gói" thành any Error — một existential container chứa metadata và pointer đến giá trị lỗi. Với typed throws, kiểu lỗi đã biết tại thời điểm biên dịch, nên giá trị được truyền trực tiếp, tiết kiệm cấp phát bộ nhớ và lookup metadata.

Trong hot path, code generic, hoặc Embedded Swift, sự khác biệt này có thể đo đếm được. Nhưng đối với UI code thông thường? Hiệu năng không phải là lý do chính để bạn refactor — type safety mới là.

Vấn Đề "Nesting Hell" và Cách Giải Quyết

Typed throws không phải không có nhược điểm. Khi bạn có nhiều tầng (data layer, repository, view model), mỗi tầng có loại lỗi riêng, việc lan truyền lỗi sẽ trở nên rườm rà. Đây là chỗ nhiều người chùn bước.

Vấn Đề Thực Tế

enum DatabaseError: Error {
    case connectionFailed
    case queryFailed
}

enum ProfileError: Error {
    case invalidInput
    case notFound
    case databaseFailure(DatabaseError)
}

func fetchUser(id: String) throws(DatabaseError) -> User {
    // Truy vấn database
}

func loadUserProfile(id: String) throws(ProfileError) -> Profile {
    guard isValidID(id) else {
        throw ProfileError.invalidInput
    }

    // Phải bọc lại lỗi DatabaseError thành ProfileError
    let user: User
    do {
        user = try fetchUser(id: id)
    } catch {
        throw ProfileError.databaseFailure(error)
    }

    return Profile(user: user)
}

Với mỗi cuộc gọi, bạn phải viết một khối do-catch để chuyển đổi lỗi. Khi có nhiều cuộc gọi liên tiếp, code trở nên lộn xộn — và đây chính là nesting hell mà cộng đồng vẫn hay than phiền.

Giải Pháp 1: Catching Protocol Pattern

Mô hình Catching protocol (được phổ biến bởi thư viện ErrorKit) cung cấp một cách thanh lịch để gói các lỗi:

public protocol Catching: Error {
    static func caught(_ error: Error) -> Self
}

extension Catching {
    public static func `catch`<ReturnType>(
        _ operation: () throws -> ReturnType
    ) throws(Self) -> ReturnType {
        do {
            return try operation()
        } catch {
            throw Self.caught(error)
        }
    }
}

Khi áp dụng vào ProfileError:

enum ProfileError: Error, Catching {
    case invalidInput
    case notFound
    case caught(Error)

    static func caught(_ error: Error) -> ProfileError {
        return .caught(error)
    }
}

func loadUserProfile(id: String) throws(ProfileError) -> Profile {
    guard isValidID(id) else {
        throw ProfileError.invalidInput
    }

    let user = try ProfileError.catch {
        try fetchUser(id: id)
    }

    return Profile(user: user)
}

Code gọn gàng, không cần do-catch lồng, và vẫn giữ type safety. Đây là pattern tôi dùng trong hầu hết dự án iOS hiện tại — nó giải quyết được phần lớn pain point khi mới chuyển sang typed throws.

Giải Pháp 2: Sử dụng Common Error Type

Khi nhiều tầng trong app cùng dùng một kiểu lỗi gốc (ví dụ AppError), việc chuyển đổi không cần thiết. Hãy thiết kế hierarchy lỗi cẩn thận từ đầu:

enum AppError: Error {
    case network(URLError)
    case database(reason: String)
    case validation(field: String)
    case unknown(Error)
}

func authenticate(email: String, password: String) async throws(AppError) -> Session {
    // Toàn bộ stack đều dùng AppError
}

Typed Throws Trong Code Generic

Một trong những use case mạnh nhất của typed throws là trong code generic. Hãy xem một version đơn giản của Sequence.map:

extension Sequence {
    public func map<T, E: Error>(
        _ transform: (Element) throws(E) -> T
    ) throws(E) -> [T] {
        var result: [T] = []
        for element in self {
            result.append(try transform(element))
        }
        return result
    }
}

Với khai báo này, kiểu lỗi từ closure transform được "truyền lên" hàm bao quanh:

  • Nếu closure không ném lỗi, map không ném lỗi (E = Never)
  • Nếu closure ném NetworkError, map ném NetworkError
  • Nếu closure ném untyped, map ném untyped (E = any Error)

Đây chính là khả năng error type polymorphism mà các ngôn ngữ khác như Java đơn giản là không có.

Tích Hợp với Swift Concurrency

Typed throws hoạt động hoàn hảo với async/await trong Swift 6.2. Cú pháp đặt throws(E) trước hoặc sau async đều hợp lệ:

actor APIClient {
    func fetchProfile(id: String) async throws(NetworkError) -> Profile {
        // ...
    }
}

@MainActor
final class ProfileViewModel {
    var profile: Profile?
    var error: NetworkError?

    func load(id: String) async {
        do {
            profile = try await client.fetchProfile(id: id)
        } catch {
            // 'error' tự động có kiểu NetworkError
            self.error = error
        }
    }
}

Kết hợp với approachable concurrency trong Swift 6.2, bạn có cả type safety về lỗi lẫn data race safety — một bước nhảy lớn về độ tin cậy của code. Cá nhân tôi nghĩ đây là combo "đáng tiền" nhất mà Swift mang lại trong vài năm qua.

Migration: Chuyển Code Cũ Sang Typed Throws

Tin vui: bạn không cần migrate toàn bộ codebase ngay lập tức. Swift 6.2 hoàn toàn tương thích ngược: func foo() throws { ... } được compiler tự động chuyển thành func foo() throws(any Error) { ... }.

Chiến Lược Migration Khuyến Nghị

  1. Bắt đầu từ tầng thấp nhất — như database, file system, networking — nơi loại lỗi rõ ràng và ổn định
  2. Chuyển dần lên các tầng cao hơn — repository, use case
  3. Giữ lại throws untyped ở public API — đặc biệt nếu bạn maintain SDK hoặc framework
  4. Đo đạc trước khi tối ưu — đừng refactor chỉ vì hiệu năng nếu chưa có bottleneck thực sự

Ví Dụ Migration Thực Tế

// Trước Swift 6
enum ImageLoaderError: Error {
    case invalidURL
    case downloadFailed
    case decodingFailed
}

func loadImage(url: URL) throws -> UIImage {
    guard let data = try? Data(contentsOf: url) else {
        throw ImageLoaderError.downloadFailed
    }
    guard let image = UIImage(data: data) else {
        throw ImageLoaderError.decodingFailed
    }
    return image
}

// Sau Swift 6.2 (typed throws)
func loadImage(url: URL) throws(ImageLoaderError) -> UIImage {
    guard let data = try? Data(contentsOf: url) else {
        throw ImageLoaderError.downloadFailed
    }
    guard let image = UIImage(data: data) else {
        throw ImageLoaderError.decodingFailed
    }
    return image
}

Sự thay đổi tối thiểu — chỉ thêm (ImageLoaderError) — nhưng giờ caller được hưởng lợi từ exhaustive switch và compiler check. Đáng lắm.

Best Practices Cho iOS Developer Năm 2026

1. Thiết Kế Error Hierarchy Cẩn Thận

Trước khi viết typed throws, hãy thiết kế cây lỗi theo domain:

enum FeatureError: Error {
    case validation(ValidationError)
    case network(NetworkError)
    case persistence(PersistenceError)
}

enum ValidationError: Error {
    case emptyField(String)
    case invalidFormat(String)
}

2. Đừng Lạm Dụng Typed Throws Cho Public API

Public API thường cần linh hoạt. Nếu bạn cam kết một kiểu lỗi cụ thể, việc thêm trường hợp mới trong tương lai có thể yêu cầu major version bump. Đây là bài học tôi học được sau khi phải bump major chỉ vì thêm một case lỗi mới.

3. Kết Hợp với LocalizedError

Để tận dụng tối đa typed throws, hãy implement LocalizedError cho enum lỗi của bạn:

enum AuthError: Error, LocalizedError {
    case invalidCredentials
    case accountLocked
    case tooManyAttempts

    var errorDescription: String? {
        switch self {
        case .invalidCredentials:
            return "Email hoặc mật khẩu không chính xác."
        case .accountLocked:
            return "Tài khoản của bạn đã bị khóa."
        case .tooManyAttempts:
            return "Quá nhiều lần thử. Vui lòng thử lại sau."
        }
    }
}

4. Test Đầy Đủ Mọi Nhánh Lỗi

Khi đã có typed throws, không có lý do gì để bỏ qua test các nhánh lỗi. Sử dụng #expect(throws:) trong Swift Testing:

import Testing

@Test
func loadUserProfile_invalidID_throwsValidationError() async {
    await #expect(throws: ProfileError.invalidInput) {
        try await loadUserProfile(id: "")
    }
}

5. Sử Dụng Compile-Time Exhaustiveness

Khi xử lý lỗi, hãy switch trên các case thay vì dùng default. Điều này đảm bảo compiler báo lỗi nếu bạn thêm case mới và quên cập nhật code xử lý — một safety net nhỏ nhưng cực kỳ hữu ích về lâu dài.

Câu Hỏi Thường Gặp (FAQ)

Typed throws có sẵn từ phiên bản Swift nào?

Typed throws được giới thiệu chính thức trong Swift 6.0 qua đề xuất SE-0413. Swift 6.2 (kèm theo Xcode 16.2 trở lên) hoàn thiện thêm các tính năng liên quan như tự động suy luận kiểu lỗi trong do-catch và tương thích với Swift Concurrency. Để dùng typed throws, bạn cần đặt minimum Swift version là 6.0 trở lên trong Package.swift hoặc target settings.

Typed throws có gây ra breaking change cho code cũ không?

Không. Compiler tự động chuyển func foo() throws { ... } thành func foo() throws(any Error) { ... }, hoàn toàn tương thích ngược. Bạn có thể migrate dần từng module mà không lo ảnh hưởng đến code hiện có.

Khi nào nên dùng typed throws thay vì Result type?

Dùng typed throws khi bạn cần lan truyền lỗi đồng bộ qua các tầng và muốn ergonomics của do-catch. Dùng Result khi bạn cần lưu trữ kết quả thành công/thất bại như một giá trị (ví dụ: trong property, callback, hoặc Combine pipeline). Trong async code, typed throws với async/await thường là lựa chọn tốt hơn.

Typed throws có cải thiện hiệu năng không?

Có, đặc biệt trong code hiệu năng cao. Khi kiểu lỗi đã biết tại thời điểm biên dịch, Swift không cần đóng gói lỗi vào existential container any Error. Điều này giảm cấp phát bộ nhớ và lookup metadata. Trong Embedded Swift hoặc hot path, sự khác biệt có thể đo đếm được. Với UI code thông thường, hiệu năng không phải lý do chính để dùng typed throws.

Làm sao tránh nesting hell khi nhiều tầng có loại lỗi khác nhau?

Có hai cách tiếp cận chính: (1) Áp dụng Catching protocol pattern để tự động bọc lỗi giữa các tầng mà không cần do-catch lồng nhau, và (2) thiết kế common error type chung cho toàn ứng dụng (như AppError) khi các tầng có liên quan chặt chẽ. Một lựa chọn thứ ba là giữ lại throws không có kiểu ở các tầng cao và chỉ dùng typed throws ở tầng thấp.

Typed throws có dùng được với protocol và generic không?

Có. Typed throws là một phần của hệ thống type của Swift và hoạt động đầy đủ với generic, protocol và associated types. Đặc biệt mạnh trong code generic như map, filter, reduce nơi compiler có thể truyền kiểu lỗi từ closure đầu vào lên hàm bao quanh — gọi là error type polymorphism.

Kết Luận

Typed throws trong Swift 6.2 đại diện cho một bước tiến lớn về error handling type-safe. Nhưng đây không phải viên đạn bạc — bạn cần áp dụng có chủ đích, hiểu rõ trade-off, và biết khi nào nên giữ throws không có kiểu.

Tóm lại các nguyên tắc quan trọng:

  • Dùng typed throws cho code nội bộ module, code generic, và code trong môi trường ràng buộc
  • Giữ throws untyped làm mặc định cho public API
  • Áp dụng Catching protocol pattern để tránh nesting hell
  • Migrate dần từ tầng thấp lên cao
  • Kết hợp với LocalizedError, Swift Testing, và Swift Concurrency để tối đa hiệu quả

Nắm vững typed throws sẽ giúp bạn viết code iOS bền vững, ít bug hơn, và dễ bảo trì hơn trong dài hạn. Theo tôi, đây là một trong những công cụ mạnh nhất mà Swift 6.2 mang lại cho iOS developer trong năm 2026 — và nếu bạn chưa thử, giờ là lúc.

Về Tác Giả Priya Raghavan

Priya spent six years at Instacart building the iOS shopper app, where she led the migration from UIKit to SwiftUI across 80+ screens and cut crash-free sessions from 99.2% to 99.87%. Before that, she was a contractor at a Bay Area design studio shipping App Store apps for two Fortune 500 retail clients. She focuses on practical SwiftUI architecture - what holds up when you have 12 engineers committing to the same codebase, not just toy MVVM examples. Her recent work involves The Composable Architecture, Swift concurrency migration audits, and reducing main-thread hangs on older devices like the iPhone XR that enterprise fleets still ship. Priya runs a small consultancy in Oakland and occasionally speaks at try! Swift NYC. She has been writing Swift since the Objective-C bridging days of 2015.