How to Handle Network Errors and Connection Failures Gracefully in Alamofire
Network errors and connection failures are inevitable when building iOS applications that interact with web services. Alamofire, being one of the most popular HTTP networking libraries for Swift, provides robust mechanisms for handling these scenarios gracefully. This comprehensive guide will walk you through various error handling strategies, from basic error detection to advanced retry mechanisms.
Understanding Alamofire Error Types
Alamofire categorizes errors into several types, each requiring different handling approaches:
AFError Types
import Alamofire
// Handle different AFError cases
AF.request("https://api.example.com/data")
.responseJSON { response in
switch response.result {
case .success(let value):
print("Success: \(value)")
case .failure(let error):
handleAlamofireError(error)
}
}
func handleAlamofireError(_ error: Error) {
if let afError = error.asAFError {
switch afError {
case .invalidURL(let url):
print("Invalid URL: \(url)")
case .parameterEncodingFailed(let reason):
print("Parameter encoding failed: \(reason)")
case .multipartEncodingFailed(let reason):
print("Multipart encoding failed: \(reason)")
case .responseValidationFailed(let reason):
print("Response validation failed: \(reason)")
case .responseSerializationFailed(let reason):
print("Response serialization failed: \(reason)")
case .sessionTaskFailed(let error):
handleSessionTaskError(error)
default:
print("Unexpected error: \(afError)")
}
}
}
Network Connection Errors
func handleSessionTaskError(_ error: Error) {
let nsError = error as NSError
switch nsError.code {
case NSURLErrorNotConnectedToInternet:
showNoInternetAlert()
case NSURLErrorTimedOut:
showTimeoutAlert()
case NSURLErrorCannotFindHost:
showHostNotFoundAlert()
case NSURLErrorCannotConnectToHost:
showConnectionFailedAlert()
case NSURLErrorNetworkConnectionLost:
showConnectionLostAlert()
case NSURLErrorBadServerResponse:
showServerErrorAlert()
default:
showGenericErrorAlert(nsError.localizedDescription)
}
}
Implementing Basic Error Handling
Simple Error Handling Pattern
func fetchUserData(completion: @escaping (Result<User, Error>) -> Void) {
AF.request("https://api.example.com/user")
.validate()
.responseDecodable(of: User.self) { response in
switch response.result {
case .success(let user):
completion(.success(user))
case .failure(let error):
// Log the error for debugging
print("Network error: \(error)")
completion(.failure(error))
}
}
}
HTTP Status Code Handling
func handleHTTPStatusCodes() {
AF.request("https://api.example.com/data")
.validate(statusCode: 200..<300)
.responseJSON { response in
switch response.result {
case .success(let value):
print("Success: \(value)")
case .failure(let error):
if let statusCode = response.response?.statusCode {
handleHTTPError(statusCode: statusCode, error: error)
}
}
}
}
func handleHTTPError(statusCode: Int, error: Error) {
switch statusCode {
case 400:
print("Bad Request - Check your parameters")
case 401:
print("Unauthorized - Authentication required")
case 403:
print("Forbidden - Access denied")
case 404:
print("Not Found - Resource doesn't exist")
case 429:
print("Too Many Requests - Rate limited")
case 500...599:
print("Server Error - Try again later")
default:
print("HTTP Error \(statusCode): \(error.localizedDescription)")
}
}
Advanced Error Handling Strategies
Custom Error Types
enum NetworkError: Error, LocalizedError {
case noInternetConnection
case serverUnavailable
case timeout
case invalidResponse
case authenticationFailed
case rateLimited(retryAfter: TimeInterval)
var errorDescription: String? {
switch self {
case .noInternetConnection:
return "No internet connection available"
case .serverUnavailable:
return "Server is currently unavailable"
case .timeout:
return "Request timed out"
case .invalidResponse:
return "Invalid response from server"
case .authenticationFailed:
return "Authentication failed"
case .rateLimited(let retryAfter):
return "Rate limited. Try again in \(Int(retryAfter)) seconds"
}
}
}
func convertToCustomError(_ error: Error) -> NetworkError {
if let afError = error.asAFError {
switch afError {
case .sessionTaskFailed(let sessionError):
let nsError = sessionError as NSError
switch nsError.code {
case NSURLErrorNotConnectedToInternet:
return .noInternetConnection
case NSURLErrorTimedOut:
return .timeout
default:
return .serverUnavailable
}
case .responseValidationFailed(let reason):
if case .unacceptableStatusCode(let code) = reason {
switch code {
case 401:
return .authenticationFailed
case 429:
return .rateLimited(retryAfter: 60)
default:
return .invalidResponse
}
}
return .invalidResponse
default:
return .serverUnavailable
}
}
return .serverUnavailable
}
Retry Mechanism Implementation
import Alamofire
class RetryHandler: RequestInterceptor {
private let maxRetryCount: Int
private let retryDelay: TimeInterval
init(maxRetryCount: Int = 3, retryDelay: TimeInterval = 1.0) {
self.maxRetryCount = maxRetryCount
self.retryDelay = retryDelay
}
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
guard request.retryCount < maxRetryCount else {
completion(.doNotRetry)
return
}
// Determine if error is retryable
if shouldRetry(error: error) {
let delay = calculateDelay(for: request.retryCount)
completion(.retryWithDelay(delay))
} else {
completion(.doNotRetry)
}
}
private func shouldRetry(error: Error) -> Bool {
if let afError = error.asAFError {
switch afError {
case .sessionTaskFailed(let sessionError):
let nsError = sessionError as NSError
return [
NSURLErrorTimedOut,
NSURLErrorNetworkConnectionLost,
NSURLErrorCannotConnectToHost
].contains(nsError.code)
default:
return false
}
}
return false
}
private func calculateDelay(for retryCount: Int) -> TimeInterval {
// Exponential backoff
return retryDelay * pow(2.0, Double(retryCount))
}
}
// Usage
let retryHandler = RetryHandler(maxRetryCount: 3, retryDelay: 1.0)
let session = Session(interceptor: retryHandler)
session.request("https://api.example.com/data")
.responseJSON { response in
// Handle response
}
Network Monitoring and Reachability
Implementing Network Reachability
import Network
import Alamofire
class NetworkMonitor {
static let shared = NetworkMonitor()
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "NetworkMonitor")
var isConnected: Bool = false
var connectionType: ConnectionType = .unknown
enum ConnectionType {
case wifi
case cellular
case ethernet
case unknown
}
private init() {
startMonitoring()
}
private func startMonitoring() {
monitor.pathUpdateHandler = { [weak self] path in
self?.isConnected = path.status == .satisfied
if path.usesInterfaceType(.wifi) {
self?.connectionType = .wifi
} else if path.usesInterfaceType(.cellular) {
self?.connectionType = .cellular
} else if path.usesInterfaceType(.wiredEthernet) {
self?.connectionType = .ethernet
} else {
self?.connectionType = .unknown
}
DispatchQueue.main.async {
NotificationCenter.default.post(
name: .networkStatusChanged,
object: self?.isConnected
)
}
}
monitor.start(queue: queue)
}
}
extension Notification.Name {
static let networkStatusChanged = Notification.Name("NetworkStatusChanged")
}
// Usage in networking layer
func makeRequest() {
guard NetworkMonitor.shared.isConnected else {
handleNetworkUnavailable()
return
}
AF.request("https://api.example.com/data")
.responseJSON { response in
// Handle response
}
}
Timeout Configuration
Custom Timeout Settings
func configureTimeouts() {
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 30.0
configuration.timeoutIntervalForResource = 60.0
let session = Session(configuration: configuration)
session.request("https://api.example.com/slow-endpoint")
.responseJSON { response in
switch response.result {
case .success(let value):
print("Success: \(value)")
case .failure(let error):
if let nsError = error as? NSError,
nsError.code == NSURLErrorTimedOut {
print("Request timed out")
// Implement timeout-specific handling
}
}
}
}
User Interface Error Handling
Error Alert Implementation
import UIKit
extension UIViewController {
func showNetworkError(_ error: NetworkError) {
let alert = UIAlertController(
title: "Network Error",
message: error.localizedDescription,
preferredStyle: .alert
)
switch error {
case .noInternetConnection:
alert.addAction(UIAlertAction(title: "Settings", style: .default) { _ in
if let settingsURL = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(settingsURL)
}
})
case .rateLimited(let retryAfter):
alert.addAction(UIAlertAction(title: "Retry Later", style: .default) { _ in
DispatchQueue.main.asyncAfter(deadline: .now() + retryAfter) {
// Retry the request
}
})
default:
alert.addAction(UIAlertAction(title: "Retry", style: .default) { _ in
// Retry the request
})
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
present(alert, animated: true)
}
}
Best Practices and Recommendations
1. Implement Comprehensive Error Logging
func logNetworkError(_ error: Error, request: URLRequest?) {
let errorDetails = [
"error": error.localizedDescription,
"url": request?.url?.absoluteString ?? "unknown",
"method": request?.httpMethod ?? "unknown",
"timestamp": ISO8601DateFormatter().string(from: Date())
]
// Log to your preferred logging service
print("Network Error: \(errorDetails)")
}
2. Use Circuit Breaker Pattern
class CircuitBreaker {
enum State {
case closed
case open
case halfOpen
}
private var state: State = .closed
private var failureCount = 0
private let failureThreshold: Int
private let timeout: TimeInterval
private var lastFailureTime: Date?
init(failureThreshold: Int = 5, timeout: TimeInterval = 60) {
self.failureThreshold = failureThreshold
self.timeout = timeout
}
func execute<T>(_ operation: @escaping () -> DataRequest) -> DataRequest? {
switch state {
case .closed:
return executeRequest(operation)
case .open:
if shouldAttemptReset() {
state = .halfOpen
return executeRequest(operation)
}
return nil // Circuit is open, reject request
case .halfOpen:
return executeRequest(operation)
}
}
private func executeRequest(_ operation: () -> DataRequest) -> DataRequest {
return operation().response { [weak self] response in
if response.error != nil {
self?.onFailure()
} else {
self?.onSuccess()
}
}
}
private func onSuccess() {
failureCount = 0
state = .closed
}
private func onFailure() {
failureCount += 1
lastFailureTime = Date()
if failureCount >= failureThreshold {
state = .open
}
}
private func shouldAttemptReset() -> Bool {
guard let lastFailureTime = lastFailureTime else { return true }
return Date().timeIntervalSince(lastFailureTime) >= timeout
}
}
3. Implement Request Queuing
class NetworkQueue {
private let queue = OperationQueue()
private var pendingRequests: [NetworkOperation] = []
init() {
queue.maxConcurrentOperationCount = 3
NotificationCenter.default.addObserver(
self,
selector: #selector(networkStatusChanged),
name: .networkStatusChanged,
object: nil
)
}
func addRequest(_ request: NetworkOperation) {
if NetworkMonitor.shared.isConnected {
queue.addOperation(request)
} else {
pendingRequests.append(request)
}
}
@objc private func networkStatusChanged() {
if NetworkMonitor.shared.isConnected {
processPendingRequests()
}
}
private func processPendingRequests() {
for request in pendingRequests {
queue.addOperation(request)
}
pendingRequests.removeAll()
}
}
Testing Error Handling
Unit Testing Network Errors
import XCTest
@testable import YourApp
class NetworkErrorHandlingTests: XCTestCase {
func testTimeoutError() {
let expectation = self.expectation(description: "Timeout error handling")
// Mock timeout error
let mockError = NSError(
domain: NSURLErrorDomain,
code: NSURLErrorTimedOut,
userInfo: nil
)
let customError = convertToCustomError(mockError)
XCTAssertEqual(customError, NetworkError.timeout)
expectation.fulfill()
waitForExpectations(timeout: 1.0)
}
func testRetryLogic() {
let retryHandler = RetryHandler(maxRetryCount: 3)
let mockRequest = MockDataRequest()
// Test retry decision for timeout error
let timeoutError = NSError(
domain: NSURLErrorDomain,
code: NSURLErrorTimedOut,
userInfo: nil
)
retryHandler.retry(mockRequest, for: Session.default, dueTo: timeoutError) { result in
switch result {
case .retryWithDelay(let delay):
XCTAssertGreaterThan(delay, 0)
case .doNotRetry:
XCTFail("Should retry timeout errors")
}
}
}
}
Conclusion
Handling network errors and connection failures gracefully in Alamofire requires a multi-layered approach that includes proper error detection, user-friendly messaging, retry mechanisms, and robust testing. By implementing the strategies outlined in this guide, you can create resilient iOS applications that provide excellent user experiences even when network conditions are poor.
For more advanced scenarios involving browser automation and handling dynamic content, consider exploring how to handle timeouts in Puppeteer or how to handle errors in Puppeteer for web scraping applications that need to interact with JavaScript-heavy websites.
Remember to always test your error handling code thoroughly, monitor network performance in production, and provide clear feedback to users when network issues occur. This comprehensive approach will help you build robust applications that users can rely on, even in challenging network conditions.