wandres.dev
MAYBE Y RESULT · sin excepciones

Cero errores en runtime: la garantía y su precio

Elm lleva años anunciándose con una promesa que ningún otro lenguaje de aplicación se atreve a firmar: sin excepciones en tiempo de ejecución. Esta lección cierra el nivel examinando esa afirmación con rigor. Primero reconstruye de dónde sale la garantía, mostrando que no es un truco sino la suma de varias decisiones que se refuerzan entre sí: ausencia de null, ausencia de excepciones, funciones totales que devuelven Maybe o Result, exhaustividad obligatoria del case, inmutabilidad, pureza y una frontera con JavaScript sellada por puertos y decodificadores. Después lee la letra pequeña y enumera con honestidad lo que la promesa no cubre. Y por último cuantifica el precio: ceremonia y verbosidad, ausencia de escotillas de emergencia, un ecosistema deliberadamente pequeño, una evolución lenta y la imposibilidad de abstraer sobre contenedores sin clases de tipos. La conclusión es que la seguridad no es gratis y que Elm elige pagarla por adelantado y a la vista.

⏱ 17 min

Ningún lenguaje de aplicación de uso general se atreve a poner en su página principal la frase que Elm lleva años exhibiendo: sin excepciones en tiempo de ejecución. No es una promesa de marketing formulada con ambigüedad calculada, sino una afirmación técnica comprobable, y equipos que mantienen bases de código de centenares de miles de líneas la han confirmado durante años con anécdotas que en cualquier otro entorno sonarían inverosímiles, del tipo un único fallo en producción en varios años de desarrollo continuo. Ahora que has visto Maybe y Result desde dentro, estás en condiciones de entender por qué la afirmación se sostiene, algo mucho más interesante que creerla. La garantía no viene de un análisis estático sofisticado ni de una demostración formal impenetrable: viene de un puñado de decisiones austeras, cada una de las cuales cierra una vía de fallo, y que juntas dejan al programa sin sitios por donde reventar. Pero toda garantía se paga, y este cierre de nivel tiene dos obligaciones: mostrar de dónde sale la promesa y, con la misma honestidad, decir qué cuesta y qué no cubre.

🎯 Al terminar esta lección sabrás
  • Reconstruir la garantía como la suma de decisiones que se refuerzan: sin null, sin excepciones, funciones totales, exhaustividad, pureza y frontera sellada.
  • Enumerar con precisión lo que la promesa no cubre y por qué esos casos residuales no la invalidan.
  • Cuantificar el precio real: ceremonia, ausencia de escotillas, ecosistema pequeño, evolución lenta y falta de abstracción sobre contenedores.
  • Formular el criterio para juzgar si esa transacción compensa en un proyecto concreto.

De dónde sale la garantía

Ninguna de las piezas basta por sí sola; el resultado nace de que ninguna deja huecos que las otras tengan que tapar. La eliminación de null suprime la desreferencia de valor ausente, que es la primera causa de fallos en producción de la industria. La eliminación de las excepciones suprime el canal de control invisible, y con él la posibilidad de que un fallo escape sin ser tratado. Las funciones parciales de la biblioteca se convierten en totales devolviendo Maybe o Result, de modo que no quedan entradas para las que el comportamiento no esté definido. La exhaustividad obligatoria del case impide dejar sin cubrir cualquiera de esos resultados. La inmutabilidad elimina la mutación concurrente y el aliasing sorpresa. La pureza garantiza que ninguna función haga nada que su tipo no anuncie.

🚫

Sin null

No existe ningún valor comodín que pueda habitar cualquier tipo. La ausencia se declara con Maybe y el compilador exige tratarla donde aparece.

📬

Sin excepciones

El fallo se devuelve como Result en vez de lanzarse. No hay segundo canal de retorno ni manejadores remotos que alguien pueda olvidar escribir.

🧱

Totalidad y exhaustividad

Toda función define una salida para toda entrada, y todo case cubre todos los constructores. No quedan grietas por donde caiga la ejecución.

🛂

Frontera sellada

El dato externo entra por puertos y decodificadores explícitos: un JSON inesperado produce un Err que tú tratas, no una excepción que te sorprende.

-- La frontera con el exterior: nada entra sin declarar su forma
type alias Chiste =
    { texto : String, votos : Int }


decodChiste : Decode.Decoder Chiste
decodChiste =
    Decode.map2 Chiste
        (Decode.field "texto" Decode.string)
        (Decode.field "votos" Decode.int)


-- Decodificar produce un Result, nunca una excepcion
-- Decode.decodeValue : Decoder a -> Value -> Result Decode.Error a

La cuarta pieza merece un párrafo aparte porque es la que suele decidir el resultado en la práctica. En la mayoría de los proyectos, los fallos de producción no nacen del código propio sino de datos ajenos que no tenían la forma prevista: un campo que el servidor dejó de enviar, un nulo donde había un número, un formato de fecha que cambió. Elm no permite que un valor externo entre en el programa sin pasar por un decodificador que declara exactamente qué forma espera, y el resultado de decodificar es un Result. La sorpresa se convierte así en un valor de fallo que tu código trata igual que cualquier otro, en el mismo instante en que ocurre y en el único punto donde puede ocurrir. Esta frontera, y no la elegancia del núcleo funcional, es la razón por la que la promesa aguanta en aplicaciones que hablan con servicios reales.

La letra pequeña

La honestidad intelectual exige leer la promesa con lupa, y Elm lo hace: la formulación cuidadosa habla de excepciones en tiempo de ejecución en la práctica, no de corrección absoluta. Estas son las grietas conocidas, y conviene tenerlas presentes para no confundir una garantía muy fuerte con una imposible.

-- Grietas reconocidas de la promesa
-- 1. Debug.todo detiene el programa a proposito: es un marcador de trabajo pendiente
siguiente : Estado -> Estado
siguiente estado =
    Debug.todo "pendiente de implementar"


-- 2. La recursion sin caso base no termina; el tipo no dice nada del tiempo
bucle : Int -> Int
bucle n =
    bucle (n + 1)


-- 3. Un Maybe mal resuelto no rompe, pero puede mentir
edad : Int
edad =
    Maybe.withDefault 0 (String.toInt "no es un numero")

Hay más matices. La aritmética no lanza: dividir entre cero produce un valor especial en coma flotante en vez de detener el programa, lo cual evita la excepción pero puede propagar un número sin sentido. La recursión muy profunda puede agotar la pila del entorno anfitrión, que es una limitación de la máquina y no del tipado. La inicialización desde JavaScript puede fallar si el nodo del documento no existe o si los datos iniciales no encajan con lo declarado. Y ningún sistema de tipos impide el error más frecuente de todos: que el programa haga exactamente lo que escribiste y lo que escribiste no fuera lo que el negocio necesitaba.

ℹ️
La promesa acota una clase de fallos, no todos los fallos

Conviene enunciar la garantía con precisión: Elm elimina una clase entera y bien delimitada de errores —los que provienen de valores ausentes, fallos no tratados, casos no cubiertos y datos externos con forma inesperada—, que resulta ser la clase más frecuente y más cara de la programación de interfaces. No promete ausencia de bugs lógicos, no promete rendimiento, no promete que tu modelo del dominio sea correcto y no promete que el usuario entienda tu interfaz. La diferencia con otros lenguajes no está en que Elm no tenga fallos, está en cuáles ha decidido hacer imposibles y en que cumple lo que dice sobre ellos.

El precio

Nada de esto es gratis, y una guía honesta debe poner la factura sobre la mesa. El primer coste es la ceremonia. Escribir un case con su rama de ausencia, o encadenar tres andThen para lo que en otro lenguaje sería un acceso encadenado con un signo de interrogación, es más código y más lento de teclear. Buena parte de ese coste desaparece con soltura y con las funciones del nivel anterior, pero no desaparece del todo, y en prototipos desechables se siente como fricción pura.

💡
La ceremonia se mide en el sitio equivocado

Cuando alguien cronometra la diferencia, casi siempre mide el tiempo de escribir la primera versión, que es el momento en que Elm sale peor parado. La medida útil es otra: el tiempo total desde que se escribe una funcionalidad hasta que deja de dar problemas, incluyendo el diagnóstico de los fallos que llegaron al usuario, las reproducciones imposibles y los arreglos hechos con prisa. En esa cuenta, las líneas de más que cuestan un case exhaustivo compiten contra tardes enteras persiguiendo un valor ausente por una traza de pila, y la comparación deja de estar reñida.

El segundo coste es la ausencia de escotillas de emergencia. En otros lenguajes con tipos fuertes existe siempre una forma de decirle al compilador confía en mí y seguir adelante. Elm no la tiene: no hay conversión insegura, no hay tipo comodín, no hay forma de llamar a JavaScript directamente desde una función. Cuando el sistema de tipos se equivoca contigo, no hay recurso, y hay que modelar el problema hasta que encaje. Es la misma decisión que produce la garantía, vista desde el otro lado.

⌨️

Ceremonia

Más pulsaciones por la misma funcionalidad. Se atenúa mucho con map y andThen, pero nunca desaparece del todo y se nota en prototipos rápidos.

🔒

Sin escotillas

No hay conversión insegura ni tipo comodín. Cuando el compilador se equivoca contigo, no hay atajo: hay que remodelar hasta que encaje.

📦

Ecosistema pequeño

Ninguna biblioteca puede incrustar JavaScript arbitrario, así que integrar código externo exige escribir puertos y decodificadores a mano.

🧬

Sin clases de tipos

No se puede escribir un map genérico para cualquier contenedor: cada módulo repite el suyo, y la abstracción sobre estructuras queda fuera de alcance.

El tercer coste es el ecosistema. La imposibilidad de incrustar JavaScript arbitrario en un paquete —una restricción endurecida deliberadamente en la versión 0.19— mantiene la garantía intacta a costa de que integrar una biblioteca externa exija escribir puertos y decodificadores a mano. La comunidad es pequeña, el ritmo de publicación del lenguaje ha sido muy pausado durante años y el gobierno del proyecto está concentrado en muy pocas manos. Y en el plano técnico, la ausencia de clases de tipos impide escribir un map genérico que sirva para cualquier contenedor: hay que repetir la misma función en cada módulo.

flowchart LR
D[Decisiones austeras] --> G[Clase de fallos imposible]
D --> P[Precio inevitable]
G --> B[Confianza al desplegar]
P --> C[Ceremonia y ecosistema pequeno]
B --> J[Juicio del proyecto]
C --> J
style G fill:#a6e3a1,color:#11111b
style P fill:#f9e2af,color:#11111b
style J fill:#cba6f7,color:#11111b

Lo que se exporta de este experimento

Antes de juzgar la transacción conviene fijar el criterio, porque no hay una respuesta universal. Un prototipo que vivirá una semana, un equipo que necesita cientos de bibliotecas del ecosistema de JavaScript o un producto cuyo mayor riesgo es llegar tarde al mercado pagan la ceremonia sin cobrar el beneficio, porque el beneficio se cobra en el mantenimiento y ese código no llegará a mantenerse. Una aplicación de larga vida, con reglas de negocio densas, con un equipo que rota y con un coste alto por cada fallo que llega al usuario, está exactamente en el otro extremo: ahí la garantía se cobra todos los días y la ceremonia se amortiza en el primer refactor grande.

Aunque nunca escribas una aplicación en Elm, sus ideas ya han llegado a tus herramientas. El tipo opcional explícito de Swift y de Kotlin, el Option y el Result de Rust con su exhaustividad obligatoria, la comprobación estricta de nulos de TypeScript y las bibliotecas de resultados que han proliferado en todos los lenguajes descienden de la misma intuición. Elm ha funcionado como un experimento controlado que llevó la idea hasta el final sin concesiones para ver qué pasaba, y lo que pasó fue que la clase de fallos desapareció. Los lenguajes mayoritarios adoptan después una versión atenuada, compatible con su legado, que captura buena parte del beneficio a cambio de renunciar a la garantía absoluta.

La seguridad no se ahorra: se paga por adelantado o se paga a plazos

El error de fondo al juzgar un lenguaje como Elm es preguntarse si la ceremonia compensa, porque la pregunta presupone que existe la opción de no pagarla. No existe. Todo programa que trate con datos que pueden faltar y con operaciones que pueden fallar tiene que decidir, en algún momento, qué ocurre en cada uno de esos casos. La única variable es cuándo se toma la decisión y quién la sufre si no se toma. Un lenguaje permisivo permite aplazar: escribes rápido, no cubres los casos, y el trabajo pendiente no se evapora, se convierte en deuda con un tipo de interés muy alto que se cobra en el peor sitio posible —en producción, con un usuario real delante, con una traza de pila que no dice qué se esperaba, a una hora en la que nadie quiere ser llamado por teléfono—. Un lenguaje como Elm cobra por adelantado: no te deja compilar hasta que la decisión esté tomada, y lo que en el permisivo era una hora que ganabas hoy y tres que perdías dentro de seis meses se convierte en veinte minutos incómodos ahora y nada después. La transacción es la misma en ambos casos; lo que cambia es la moneda, el momento y el tipo de interés. Y hay un segundo efecto, más sutil y probablemente más valioso, que solo se percibe habiendo trabajado así una temporada: cuando el compilador garantiza la ausencia de una clase entera de fallos, la atención que dedicabas a vigilarla queda libre para el problema real. Dejas de leer código preguntándote si esto podrá ser nulo aquí y empiezas a leerlo preguntándote si esto modela bien el dominio. Refactorizas sin miedo, porque el compilador enumera exhaustivamente lo que se rompe. Desplegar un viernes deja de ser una temeridad. Ese cambio en la relación con el propio código es la verdadera aportación de Elm, y sobrevive perfectamente a la decisión práctica de no usar Elm: el hábito de declarar la ausencia, devolver el fallo y modelar el error viaja con quien lo aprendió a cualquier lenguaje que le permita ejercerlo.

⚔️ Somete la promesa a examen
  1. Enumera las seis decisiones que sostienen la garantía y explica qué vía de fallo cierra cada una por separado.
  2. Provoca los tres casos residuales del ejemplo y comprueba cuál detiene el programa, cuál no termina y cuál produce un dato falso sin romper nada.
  3. Escribe un decodificador que rechace un JSON con un campo ausente y razona por qué esa frontera explica la mayor parte de la fiabilidad observada.
  4. Cronometra el tiempo que tardas en cubrir todos los casos de un Result y compáralo con el que dedicaste la última vez a diagnosticar un fallo en producción.
  5. Localiza en un lenguaje que uses a diario la versión atenuada de estas ideas y describe con precisión qué concesión hace y qué garantía pierde a cambio.
  6. Defiende y después refuta el uso de Elm en un proyecto real con equipo, plazos y dependencias; concluye con el criterio que usarías para decidir.