Swift introdujo el manejo de errores estructurado en la versión 2.0, reemplazando los patrones de punteros a errores de Objective-C con la semántica nativa de throw y catch. La palabra clave rethrows surgió para resolver la fricción específica donde funciones genéricas de orden superior como map o filter obligaban a los llamadores a usar try incluso al pasar cierres que no lanzan excepciones, creando una ceremonia innecesaria de manejo de errores.
El problema se centra en la polimorfismo de efectos de funciones y la subtipación. En el sistema de tipos de Swift, un cierre que no lanza excepciones es un subtipo de un cierre que lanza excepciones porque satisface el contrato de "puede lanzar" al nunca lanzar. Sin rethrows, una función que acepta un cierre que lanza excepciones debe propagar incondicionalmente las excepciones, obligando a todos los sitios de llamada a manejar errores independientemente del comportamiento real del argumento.
La solución es la anotación rethrows, que establece un contrato condicional: la función solo lanza si su parámetro de cierre lanza. El compilador de Swift implementa esto al rastrear el tipo de lanzamiento de los argumentos de cierre en tiempo de compilación. Cuando se pasa un cierre que no lanza excepciones, la función se trata como no lanzadora en el sitio de llamada, eliminando la necesidad de try; cuando se pasa un cierre que lanza excepciones, la función hereda el efecto de lanzamiento.
Estábamos construyendo un pipeline de transformación de datos modular para una aplicación de iOS donde los usuarios podían encadenar operaciones como el análisis de JSON, el cambio de tamaño de imágenes y el hash criptográfico. La función central pipeline aceptaba un arreglo de transformaciones definidas como (Data) throws -> Data. Inicialmente, usamos una anotación estándar throws en pipeline, lo que obligó a cada sitio de llamada a envolver incluso transformaciones simples en bloques do-catch a pesar de que muchas operaciones eran funciones puras sin modos de fallo.
Nuestro primer enfoque duplicó la función completa: una versión llamada pipeline para transformaciones no lanzadoras y otra llamada pipelineThrowing para las que lanzan. Esta separación permitió sitios de llamada limpios pero creó una pesadilla de mantenimiento donde cada corrección de errores requería editar dos ubicaciones, y la superficie de API se duplicaba con cada nueva opción de configuración. Además, los usuarios debían conocer detalles de implementación para elegir el método correcto, violando principios de encapsulación.
El segundo enfoque mantuvo una única firma throws pero animó a usar try? para silenciar advertencias, efectivamente descartando información de errores y haciendo imposible la depuración cuando ocurrían errores reales. Esto violaba las garantías de seguridad y hacía que el código fuera frágil, ya que los desarrolladores olvidarían manejar casos de error genuinos en pipelines mixtos que contenían operaciones tanto seguras como inseguras.
Finalmente, adoptamos la solución rethrows, declarando func pipeline(_ transforms: [(Data) throws -> Data]) rethrows -> Data. Esto permitió que el compilador hiciera cumplir try solo cuando el arreglo de cierres contenía operaciones que lanzan excepciones, mientras permitía llamadas directas para cálculos puros. El resultado fue una reducción del 40% en el código repetitivo, la eliminación de firmas de funciones duplicadas y una mejora en la ergonomía de la API donde el sistema de tipos reflejaba con precisión los dominios de error reales de casos de uso específicos.
¿Por qué Swift prohíbe lanzar errores directamente dentro del cuerpo de una función rethrows en lugar de exclusivamente a través del parámetro de cierre?
La palabra clave rethrows crea un estricto contrato de transparencia que establece que la función solo propaga errores generados por sus argumentos. Si intentas throw CustomError() directamente en el cuerpo de la función, el compilador de Swift lo rechaza porque esto representa un lanzamiento incondicional, violando la garantía de "solo si el cierre lanza". La función debe manejar sus propios errores internamente utilizando do-catch, convertirlos en valores de retorno, o escalar la firma a throws incondicional, asegurando que los llamadores puedan asumir de manera segura que no se originan nuevos dominios de error desde la propia función.
¿Cómo interactúa rethrows con múltiples parámetros de cierre y cuáles son las implicaciones para la propagación de efectos?
Cuando una función tiene múltiples parámetros de cierre marcados como lanzadores y la función misma está marcada como rethrows, la función lanza si cualquiera de los cierres lanza, creando una unión de efectos. El compilador de Swift rastrea estos efectos individualmente a lo largo de la cadena de llamadas, por lo que componer funciones rethrows preserva la naturaleza condicional sin intervención manual. Sin embargo, si transformas o envuelves los cierres antes de pasarlos, debes preservar la firma de lanzamiento en el envoltorio, o el compilador tratará el argumento como no lanzador, haciendo que la función externa pierda su capacidad de lanzamiento condicional.
¿Cuál es la relación entre rethrows y @autoclosure, y por qué aparece este patrón en las APIs de afirmación?
La combinación de @autoclosure y rethrows permite la evaluación perezosa con la propagación condicional de errores, donde la autoclosura retrasa la evaluación hasta que se necesite y la función solo lanza si esa evaluación retrasada lanza. Este patrón alimenta las funciones assert y precondition de Swift, permitiendo pasar expresiones que lanzan excepciones a afirmaciones sin marcar la llamada de afirmación con try. Los candidatos a menudo pasan por alto que la autoclosura debe declarar explícitamente () throws -> T para participar en el contrato rethrows, y que este mecanismo separa el tiempo de evaluación (perezoso) de la semántica de propagación de errores (condicional), lo cual es crucial para rutas críticas de rendimiento donde las afirmaciones están deshabilitadas en las versiones de producción.