Table of contents

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.

Try WebScraping.AI for Your Web Scraping Needs

Looking for a powerful web scraping solution? WebScraping.AI provides an LLM-powered API that combines Chromium JavaScript rendering with rotating proxies for reliable data extraction.

Key Features:

  • AI-powered extraction: Ask questions about web pages or extract structured data fields
  • JavaScript rendering: Full Chromium browser support for dynamic content
  • Rotating proxies: Datacenter and residential proxies from multiple countries
  • Easy integration: Simple REST API with SDKs for Python, Ruby, PHP, and more
  • Reliable & scalable: Built for developers who need consistent results

Getting Started:

Get page content with AI analysis:

curl "https://api.webscraping.ai/ai/question?url=https://example.com&question=What is the main topic?&api_key=YOUR_API_KEY"

Extract structured data:

curl "https://api.webscraping.ai/ai/fields?url=https://example.com&fields[title]=Page title&fields[price]=Product price&api_key=YOUR_API_KEY"

Try in request builder

Related Questions

Get Started Now

WebScraping.AI provides rotating proxies, Chromium rendering and built-in HTML parser for web scraping
Icon