Never
Never es un tipo vacío que no tiene valores posibles. Indica que “esto nunca puede ocurrir” o “esto nunca devuelve un valor”.
Cuando el Failure es Never, Combine entiende que un Publisher nunca falla, porque Never no tiene valores posibles (es decir, no puede existir un error de tipo Never).
Usar Never como tipo de error simplifica el manejo de flujos seguros o “infalibles”. Esto permite suscribirse sin necesidad de manejar errores:
[1, 2, 3].publisher.sink { print($0)}
Si el error no fuera Never tendría que manejarlo de forma obligatoria:
publisher
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion { print(error) }
}, receiveValue: { ... })
setFailureType()
Hay escenarios donde necesito combinar un Publisher que nunca falla (e.g. Publisher) con otro que sí (e.g. Publisher) con una operación que requiere que los dos Publishers sean del mismo tipo (e.g. combineLatest(_:)). En este caso, setFailureType cambia el tipo de error emitido por un Publisher de entrada que no emite errores.
[1, 2, 3].publisher
.setFailureType(to: NameError.self)
.sink(receiveCompletion: { completion in
// Notar que este método está disponible gracias a setFailureType
if case .failure(let error) = completion { print(error) }
}, receiveValue: { ... })
assign(to:on:)
El operador assign(to:on:) solo funciona con Publishers que no pueden arrojar errores puesto que al tratar de enviar a un error a una propiedad de una clase, quedaría un error sin manejar o tendríamos un comportamiento indefinido.
Al usar assign(to:on:) sobre un Publisher que arroja un error se obtiene el siguiente error de compilación:
referencing instance method ‘assign(to🔛)’ on ‘Publisher’ requires the types ‘any Error’ and ‘Never’ be equivalent
assertNoFailure()
assertNoFailure(_:file:line:) levanta un error fatal cuando recibe un error de parte del stream de entrada. De lo contrario, vuelve a emitir el valor recibido.
Este operador convierte al stream de salida en un Publisher con Failure=Never, lo que permite aplicar operadores seguros como sink(receiveValue:).
Just("Hello")
.tryMap { _ in throw MyError.ohNo }
// Arroja error cuando recibe "Hello"
.assertNoFailure() // La aplicación se explota
.sink(receiveValue: { print($0) })
Manejando fallas
Operadores try*
Algunos operadores de Combine, como map(_:), reciben un closure de transformación que convierte una entrada en otra, pero NO PUEDE ARROJAR ERRORES. Esto asegura que, al menos esa etapa del pipeline de Combine no arroja errores.
Ante esta problemática, existe otro tipo de operadores con prefijo “try“, como tryMap(_:), que también reciben una función de transformación pero que SI PUEDEN arrojar errores.
En caso de querer arrojar un error desde un operadores que no tiene el prefijo “try“, se obtiene el error de compilación:
Invalid conversion from throwing function of type ‘(X) throws -> Y’ to non-throwing function type ‘(X) -> Y’
Mapeando errores
Cualquier operador con sufijo “try” convierte el pipeline de Combine en un Publisher con error genérico (Failure=Swift.Error). El siguiente código arroja un error de compilación incluso aunque haya completa certeza de que tryMap va a arrojar un error de tipo NameError, e incluso que el Publisher de entrada sea de tipo Publisher debido al uso del operador setFailureType:
Just("Hello")
.setFailureType(to: NameError.self)
.tryMap { throw NameError.tooShort($0) }
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("¡Listo!")
// ¡Error de compilación!
case .failure(.tooShort(let name)):
print("(name) es muy corto")
case .failure(.unknown):
print("Error inesperado")
}
}, receiveValue: { print("Se recibió el valor ($0)") })
.store(in: &subscriptions)
El operador mapError convierte un error recibido de un stream de entrada, en otro error:
Just("Hello")
.setFailureType(to: NameError.self)
.tryMap { throw NameError.tooShort($0) }
// ¡Sí funciona con mapError!
.mapError { $0 as? NameError ?? .unknown }
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("¡Listo!")
case .failure(.tooShort(let name)):
print("(name) es muy corto")
case .failure(.unknown):
print("Error inesperado")
}
}, receiveValue: { print("Se recibió el valor ($0)") })
.store(in: &subscriptions)
Diseño de APIs que pueden fallar
Al encadenar varias operaciones que emitan errores de tipos diferentes, el Publisher final será de tipo Publisher. En caso de querer normalizar los errores, se debe usar mapError como en el siguiente ejemplo:
let decoder = JSONDecoder()
return URLSession.shared
.dataTaskPublisher(for: request)
.tryMap({ data, _ -> Data in
if let decodedError = try? decoder.decode(ServerError.self, from: data) {
throw Error.jokeDoesntExist(id: id)
}
return data
})
.decode(type: Joke.self, decoder: decoder)
.mapError { error -> DadJokes.Error in
switch error {
case is URLError:
return .network
case is DecodingError:
return .parsing
default:
return error as? DadJokes.Error ?? .unknown
}
}
En este caso, usar .map(.data) no permitía capturar el error devuelto por el servidor, así que mejor se usó .tryMap para tratar de extraerlo y, en caso de lograrlo, emitir el error Error.jokeDoesntExist(id: id).
Finalmente, mapError se usó para convertir cualquier tipo de error (any Error) en valores específicos de tipo DadJokes.Error.
Atrapando y reintentando
Muchas veces la falla de alguna operación puede ser debida a un problema de conexión a internet u otro recurso no disponible, que quizás pueda funcionar correctamente al intentarlo de nuevo.
retry(_:) se re-suscribe al stream de entrada cuando recibe un evento de error, y reintenta el número de veces que se le especifique por parámetro (i.e. retries). Si todos los reintentos fallan, emite el error recibido como si el operador retry(_:) no existiera.
replaceError(with:) reemplaza todo error del stream de entrada por un valor por defecto. Esto también cambia el tipo de falla (i.e. Failure) del Publisher a Never puesto que todo error se reemplaza con un valor.
catch(_:) captura un error emitido por un Publisher y lo reemplaza con otro Publisher diferente.
Consideremos el siguiente ejemplo:
photoService
.fetchPhoto(quality: .high) // 1
.retry(3) // 2
.catch { error -> PhotoService.Publisher in // 3
return photoService.fetchPhoto(quality: .low)
}
.replaceError(with: UIImage(named: "na.jpg")!) // 4
.sink( ... )
.store(in: &subscriptions)
Aquí,
- Se trata de descargar una foto en alta calidad.
- Si la descarga en alta calidad falla, se reintenta la petición 3 veces.
- Si la descarga en alta calidad falla una cuarta vez (la primera más tres reintentos), entonces se reemplaza el Publisher de entrada por otro que descarga una foto en baja calidad.
- Si la descarga en baja calidad falla, se emite un valor por defecto (i.e. UIImage(named: “na.jpg”))
Cuestionario
1. ¿Qué representa el tipo Never en Swift?
a) Un tipo que puede representar cualquier valor.
b) Un tipo que indica una operación asincrónica.
c) Un tipo vacío que no tiene valores posibles.
d) Un tipo de error genérico de Combine.
2. ¿Qué significa que un Publisher tenga Failure = Never?
a) Que nunca emite valores.
b) Que nunca falla.
c) Que sólo puede fallar con errores del sistema.
d) Que no emite valores después del primer error.
3. ¿Para qué sirve el operador setFailureType(to:)?
a) Para forzar que un Publisher falle siempre.
b) Para combinar Publishers de diferentes tipos de Output.
c) Para cambiar el tipo de error de un Publisher que no falla.
d) Para capturar errores y reemplazarlos con valores por defecto.
4. ¿Por qué el operador assign(to:on:) solo funciona con Publishers infalibles?
a) Porque assign no sabe cómo manejar errores.
b) Porque sólo trabaja con valores de tipo Never.
c) Porque necesita un DispatchQueue de Combine.
d) Porque es exclusivo de ObservableObject.
5. ¿Qué hace el operador assertNoFailure()?
a) Convierte un Publisher infalible en uno que puede fallar.
b) Reintenta la suscripción si ocurre un error.
c) Lanza un error fatal si recibe un error.
d) Reemplaza errores con valores nulos.
6. ¿Cuál es la diferencia principal entre map y tryMap en Combine?
a) map puede arrojar errores y tryMap no.
b) tryMap puede arrojar errores y map no.
c) Ambos pueden arrojar errores, pero tryMap los ignora.
d) map convierte errores en valores opcionales.
7. Cuando se usa un operador con prefijo try, ¿qué ocurre con el tipo del Publisher resultante?
a) Su Failure cambia a Never.
b) Su Failure cambia a Swift.Error.
c) Su Output se convierte en Optional.
d) Se convierte automáticamente en un Just.
8. ¿Qué propósito tiene el operador mapError?
a) Cambiar el tipo de valor emitido por el Publisher.
b) Convertir un error recibido en otro tipo de error.
c) Ignorar errores y continuar el flujo.
d) Volver infalible el Publisher.
9. ¿Qué hace el operador retry(_:) en Combine?
a) Reintenta la suscripción al Publisher cuando ocurre un error.
b) Reemplaza errores por valores predeterminados.
c) Detiene el flujo después del primer error.
d) Convierte errores en Never.
10. En el siguiente fragmento de código, ¿qué operador evita que la aplicación falle mostrando una imagen por defecto?
photoService
.fetchPhoto(quality: .high)
.retry(3)
.catch { error -> PhotoService.Publisher in
return photoService.fetchPhoto(quality: .low)
}
.replaceError(with: UIImage(named: "na.jpg")!)
a) retry(3)
b) catch
c) replaceError(with:)
d) mapError
Solución
1. ¿Qué representa el tipo Never en Swift?
c) Un tipo vacío que no tiene valores posibles. ✅
2. ¿Qué significa que un Publisher tenga Failure = Never?
b) Que nunca falla. ✅
3. ¿Para qué sirve el operador setFailureType(to:)?
c) Para cambiar el tipo de error de un Publisher que no falla. ✅
4. ¿Por qué el operador assign(to:on:) solo funciona con Publishers infalibles?
a) Porque assign no sabe cómo manejar errores. ✅
5. ¿Qué hace el operador assertNoFailure()?
c) Lanza un error fatal si recibe un error. ✅
6. ¿Cuál es la diferencia principal entre map y tryMap en Combine?
b) tryMap puede arrojar errores y map no. ✅
7. Cuando se usa un operador con prefijo try, ¿qué ocurre con el tipo del Publisher resultante?
b) Su Failure cambia a Swift.Error. ✅
8. ¿Qué propósito tiene el operador mapError?
b) Convertir un error recibido en otro tipo de error. ✅
9. ¿Qué hace el operador retry(_:) en Combine?
a) Reintenta la suscripción al Publisher cuando ocurre un error. ✅
10. En el siguiente fragmento de código, ¿qué operador evita que la aplicación falle mostrando una imagen por defecto?
photoService
.fetchPhoto(quality: .high)
.retry(3)
.catch { error -> PhotoService.Publisher in
return photoService.fetchPhoto(quality: .low)
}
.replaceError(with: UIImage(named: "na.jpg")!)
c) replaceError(with:) ✅