Manejo de errores en Swift: try/catch y más (nivel avanzado)
\n\nEl manejo de errores es uno de los temas clave en el desarrollo con Swift. Si ya estás familiarizado con lo básico de do-catch y try?, es hora de pasar al siguiente nivel. En este artículo, exploraremos técnicas avanzadas: trabajar con el protocolo Error, el tipo Result, la palabra clave rethrows, y cómo diseñar correctamente una jerarquía de errores en proyectos reales de iOS/macOS.
Inmersión profunda en el protocolo Error y errores personalizados
\n\nEn Swift, cualquier error debe cumplir con el protocolo Error. Sin embargo, para hacer el código más legible y mantenible, vale la pena crear enumeraciones (enum) propias con casos anidados y valores asociados.
enum NetworkError: Error {\n case invalidURL\n case noData\n case decodingFailed(description: String)\n case serverError(statusCode: Int)\n case timeout(seconds: Double)\n}\n\n// Ejemplo de uso\nfunc fetchUser(id: Int) throws -> User {\n guard id > 0 else {\n throw NetworkError.invalidURL\n }\n // ... lógica de la solicitud\n if someCondition {\n throw NetworkError.serverError(statusCode: 500)\n }\n return User()\n}\n\nEste enfoque permite transmitir contexto adicional: código de error, descripción o tiempo de espera. Esto es especialmente útil para el registro y la depuración.
\n\nResult: alternativa a do-catch para código asíncrono
\n\nEl tipo Result<Success, Failure> es una herramienta poderosa que proviene de la programación funcional. Es ideal para APIs basadas en callbacks, solicitudes de red y operaciones asíncronas.
enum DataError: Error {\n case corrupted\n case notFound\n}\n\nfunc loadImage(from url: URL, completion: @escaping (Result<UIImage, DataError>) -> Void) {\n DispatchQueue.global().async {\n // Simulación de carga\n if arc4random_uniform(2) == 0 {\n completion(.success(UIImage()))\n } else {\n completion(.failure(.notFound))\n }\n }\n}\n\n// Uso\nloadImage(from: someURL) { result in\n switch result {\n case .success(let image):\n print("Imagen cargada: \\(image)")\n case .failure(let error):\n print("Error: \\(error.localizedDescription)")\n }\n}\n\nResult hace que el código sea más predecible y elimina la necesidad de manejar valores opcionales con nil. Además, puedes usar .map y .flatMap para cadenas de transformaciones.
Rethrows: cuando una función «propaga» errores
\n\nLa palabra clave rethrows es una de las características más infravaloradas de Swift. Permite escribir una función que no lanza errores por sí misma, pero los «propaga» si el closure pasado es throws.
func performOperation<T>(on array: [T], operation: (T) throws -> Bool) rethrows -> [T] {\n var result: [T] = []\n for element in array {\n if try operation(element) {\n result.append(element)\n }\n }\n return result\n}\n\n// Sin errores (no necesita try)\nlet numbers = [1, 2, 3, 4, 5]\nlet evenNumbers = performOperation(on: numbers) { $0.isMultiple(of: 2) }\nprint(evenNumbers) // [2, 4]\n\n// Con error (necesita try)\nenum ValidationError: Error { case negative }\nlet filtered = try performOperation(on: numbers) { value in\n guard value > 0 else { throw ValidationError.negative }\n return value % 2 == 0\n}\n\nrethrows hace que la API sea flexible: el usuario puede pasar tanto un closure normal (sin try) como un closure con throws (entonces se requerirá try). Esto se usa a menudo en la biblioteca estándar de Swift, por ejemplo, en map, filter y for.