You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift错误处理最佳实践咨询及Alamofire场景处理建议

Hey there! I totally get where you're coming from—switching from Objective-C's familiar @try/@catch to Swift's error handling model can feel a bit jarring at first. Let's break down the best practices and practical tricks, especially for handling Alamofire's closure-based service calls.

Swift Error Handling: Core Basics & General Best Practices

First, let's recap how Swift's error model differs from Objective-C: it's explicit and focused on expected, recoverable errors (rather than catching all runtime crashes like OC's @try/@catch). Here are the key patterns to master:

1. Define Custom Error Types

Start by creating a dedicated error enum that conforms to Swift's Error protocol (you can even extend it for user-facing messages later):

enum AppError: Error {
    case networkFailure(String)
    case invalidServerResponse
    case dataParsingFailed(String)
    case missingRequiredData
    case unknownError
}

// Add localized messages for user-facing UI
extension AppError: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .networkFailure(let message):
            return NSLocalizedString(message, comment: "Network error detail")
        case .invalidServerResponse:
            return NSLocalizedString("Server returned an invalid response", comment: "")
        case .dataParsingFailed(let message):
            return NSLocalizedString("Failed to parse data: \(message)", comment: "")
        case .missingRequiredData:
            return NSLocalizedString("Missing critical data from server", comment: "")
        case .unknownError:
            return NSLocalizedString("An unexpected error occurred", comment: "")
        }
    }
}

2. Use throw + do/catch for Synchronous Code

Replace OC's @try/@catch with Swift's throw (to signal errors) and do/catch (to handle them). Pair this with guard to avoid nested conditionals:

func parseUser(from data: Data) throws -> User {
    guard let jsonObject = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
        throw AppError.dataParsingFailed("Invalid JSON format")
    }
    
    guard let id = jsonObject["id"] as? Int,
          let name = jsonObject["name"] as? String else {
        throw AppError.missingRequiredData
    }
    
    return User(id: id, name: name)
}

// Usage example
do {
    let user = try parseUser(from: rawData)
    // Use the user object
} catch let error as AppError {
    print("Parsing error: \(error.localizedDescription)")
} catch {
    print("Unexpected error: \(error)")
}

3. Leverage Result for Asynchronous Code

Since you can't throw directly from async closures (like Alamofire's completion blocks), Swift's Result<Success, Failure> type is your best friend. It wraps either a success value or an error, making async error handling predictable.


Alamofire Closure-Specific Error Handling

Alamofire uses its own AFError type for network-related issues, but you'll want to map these to your custom AppError for consistency across your app. Here's a step-by-step example:

1. Wrap Alamofire Requests with Custom Error Mapping

Create a reusable network layer (e.g., a NetworkManager singleton) to encapsulate Alamofire calls and error conversion:

class NetworkManager {
    static let shared = NetworkManager()
    
    private init() {}
    
    func fetchUser(userId: Int, completion: @escaping (Result<User, AppError>) -> Void) {
        let url = "https://your-api-endpoint.com/users/\(userId)"
        
        AF.request(url)
            .validate(statusCode: 200..<300) // Auto-fails for non-2xx status codes
            .responseData { response in
                switch response.result {
                case .success(let data):
                    // Handle parsing with our synchronous throwing function
                    do {
                        let user = try self.parseUser(from: data)
                        completion(.success(user))
                    } catch let parsingError as AppError {
                        completion(.failure(parsingError))
                    } catch {
                        completion(.failure(.unknownError))
                    }
                    
                case .failure(let afError):
                    // Map Alamofire's AFError to our custom AppError
                    let appError = self.convertAFError(to: afError)
                    completion(.failure(appError))
                }
            }
    }
    
    private func convertAFError(to afError: AFError) -> AppError {
        switch afError {
        case .invalidURL, .parameterEncodingFailed:
            return .networkFailure("Invalid request configuration")
        case .responseValidationFailed(let reason):
            switch reason {
            case .unacceptableStatusCode(let code):
                return .networkFailure("Server returned error code: \(code)")
            default:
                return .networkFailure("Server response validation failed")
            }
        case .responseSerializationFailed:
            return .invalidServerResponse
        default:
            return .networkFailure(afError.localizedDescription)
        }
    }
    
    private func parseUser(from data: Data) throws -> User {
        // Reuse the parsing function we defined earlier
        guard let jsonObject = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
            throw AppError.dataParsingFailed("Invalid JSON format")
        }
        
        guard let id = jsonObject["id"] as? Int,
              let name = jsonObject["name"] as? String else {
            throw AppError.missingRequiredData
        }
        
        return User(id: id, name: name)
    }
}

2. Consume the Network Call with Clear Error Handling

When using the network layer, handle the Result with a switch statement for clarity:

NetworkManager.shared.fetchUser(userId: 123) { result in
    switch result {
    case .success(let user):
        print("Successfully fetched user: \(user.name)")
        // Update UI with user data
    case .failure(let error):
        print("Error fetching user: \(error.localizedDescription)")
        // Show error alert to the user
    }
}

3. Upgrade to async/await (Alamofire 5.5+)

If you're using a newer version of Alamofire, you can take advantage of Swift's async/await to make error handling feel more like synchronous code:

extension NetworkManager {
    func fetchUser(userId: Int) async throws -> User {
        let url = "https://your-api-endpoint.com/users/\(userId)"
        let data = try await AF.request(url)
            .validate(statusCode: 200..<300)
            .serializingData()
            .value
        
        return try parseUser(from: data)
    }
}

// Usage in an async context (e.g., a ViewModel)
Task {
    do {
        let user = try await NetworkManager.shared.fetchUser(userId: 123)
        // Use user data
    } catch let appError as AppError {
        // Handle custom app errors
        print(appError.localizedDescription)
    } catch let afError as AFError {
        // Handle any un-mapped Alamofire errors
        print("Alamofire error: \(afError)")
    } catch {
        // Catch-all for unexpected errors
        print("Unknown error: \(error)")
    }
}

Final Pro Tips

  • Avoid overusing try?: It converts errors to nil, which can hide bugs. Only use it when you truly don't care about the error (e.g., optional data that's not critical).
  • Centralize error handling: Create a helper function or extension to map all external errors (like AFError, URLError) to your custom AppError—this keeps your code consistent.
  • Distinguish between recoverable and fatal errors: Swift's throw is for recoverable issues (network blips, bad data). For fatal errors (e.g., invalid app state), use preconditionFailure() or assert() during development to catch issues early.

内容的提问来源于stack exchange,提问作者Masashi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 08:14:51