wandres.dev
EL COMPILADOR AMIGO · errores que enseñan

Mensajes de error que enseñan: la conquista que Elm exportó a media industria

Los mensajes de error de Elm son probablemente la contribución del lenguaje que más lejos ha viajado, y esta lección reconstruye por qué. Empieza por el giro conceptual que los hizo posibles: dejar de tratar el diagnóstico como un volcado de estado interno del compilador y empezar a tratarlo como un texto escrito para una persona concreta en un momento concreto de frustración, con la consecuencia organizativa de que la calidad de los mensajes pasó a ser una funcionalidad con su propio repositorio, sus informes y sus revisiones. Después disecciona la anatomía de un mensaje completo, elemento por elemento y en el orden exacto en que aparecen, mostrando qué trabajo cognitivo hace cada pieza. A continuación rastrea la descendencia documentada en Rust, en TypeScript, en Python y en Scala, distinguiendo lo que se copió de verdad de lo que solo se parece por fuera. Y termina con la tesis incómoda: la calidad de un mensaje de error no es una cuestión de esmero sino una consecuencia del diseño del lenguaje, y hay lenguajes que no pueden producir mensajes así por mucho que lo intenten.

⏱ 18 min

De todo lo que Elm ha aportado a la programación, hay una aportación que ha viajado mucho más lejos que la arquitectura unidireccional, más lejos que los efectos como datos y más lejos incluso que la idea de modelar la ausencia con un tipo: sus mensajes de error. Es un caso raro de influencia, porque no se trata de una abstracción ni de un patrón sino de una decisión sobre cómo se le habla a una persona. Antes de 2015, un mensaje de error de compilador era un artefacto que nadie había diseñado: era el estado interno del algoritmo de comprobación, serializado sin cuidado, en la jerga técnica del implementador, en el punto del código donde la comprobación falló y no en el punto donde estaba el problema. Todo el mundo lo aceptaba como se aceptan las cosas que llevan tanto tiempo siendo así que ya no se ven. Elm hizo la pregunta obvia que nadie había hecho en serio, que es para quién se escribe esto, y al responderla descubrió que la respuesta cambiaba el artefacto entero. Esta lección abre el nivel del compilador como copiloto por donde debe abrirse: por el momento en que el compilador dejó de ser un juez que dicta sentencia y empezó a ser un colega que te explica lo que ha visto.

🎯 Al terminar esta lección sabrás
  • Reconstruir el giro de diseño que convirtió el diagnóstico en un texto dirigido a una persona y no en un volcado del estado del compilador.
  • Diseccionar la anatomía completa de un mensaje de Elm y justificar el trabajo cognitivo que hace cada pieza y su orden.
  • Rastrear con precisión qué adoptaron Rust, TypeScript, Python y Scala, y separar la herencia real de la coincidencia superficial.
  • Argumentar por qué la calidad del diagnóstico depende del diseño del lenguaje y no del esmero del implementador.

El compilador cambia de destinatario

El giro se puede enunciar en una frase: un mensaje de error es una pieza de interfaz de usuario, y hay que diseñarla como tal. De esa frase salen consecuencias que parecen menores y no lo son. Si es interfaz, tiene un usuario, y ese usuario no es el implementador del compilador sino alguien que probablemente lleva veinte minutos peleando con algo, que no conoce la teoría de tipos subyacente y que necesita saber tres cosas en este orden: qué pasa, dónde pasa y qué puede hacer. Si es interfaz, se puede probar con usuarios reales, se puede iterar y se puede considerar defectuosa aunque sea técnicamente correcta. Y si es interfaz, su calidad es una funcionalidad del producto, no un detalle de implementación.

Elm llevó esa consecuencia organizativa hasta el final. Los mensajes dejaron de ser una cadena de texto escondida en el código del compilador y pasaron a tener repositorio propio, con informes donde cualquiera podía pegar un mensaje que le había resultado confuso y proponer una redacción mejor. La consigna que gobernaba las revisiones era simple y radical: si el usuario se quedó atascado, el mensaje es culpable, aunque diga la verdad. Un diagnóstico veraz que no ayuda es un diagnóstico fallido.

-- NAMING ERROR --------------------------------------------- src/Main.elm

I cannot find a `vieChiste` variable:

45|     div [] [ vieChiste modelo ]
                 ^^^^^^^^^
These names seem close though:

    viewChiste
    viewChistes
    viewChisteBreve

Hint: Read <https://elm-lang.org/0.19.1/imports> to see how `import`
declarations work in Elm.
🧑

Un destinatario humano

El texto se dirige a una persona en primera persona y en lenguaje natural. Nada de vocabulario del algoritmo de unificación ni de nombres internos del compilador.

📍

El sitio correcto

El acento circunflejo señala la expresión culpable, no la línea donde la comprobación tropezó. Encontrar el sitio es la mitad del trabajo de depurar.

🧭

Una salida propuesta

Después de decir qué falla, el mensaje sugiere qué hacer. La sugerencia puede fallar, pero convierte un callejón sin salida en una hipótesis comprobable.

🤝

Sin condescendencia

El tono evita tanto el reproche como la falsa alegría. Asume que el error es normal, que quien lo lee sabe programar y que la información basta.

Anatomía de un mensaje

Un mensaje completo de Elm tiene seis piezas y el orden no es casual: cada una responde a la pregunta que deja abierta la anterior. La cabecera clasifica el fallo en una familia con nombre en mayúsculas, lo cual permite reconocerlo antes de leerlo y buscarlo cuando se repite. La referencia al archivo sitúa el problema en el proyecto. El fragmento de código con su número de línea reproduce el contexto para que no haya que cambiar de ventana. Los acentos circunflejos acotan la expresión responsable con precisión de columna. La explicación en prosa dice qué se esperaba y qué se encontró, en ese orden, porque la expectativa es lo que el lector no conoce. Y la pista final propone una acción o remite a una explicación más larga.

-- TYPE MISMATCH ------------------------------------------- src/Main.elm

The 2nd argument to `String.repeat` is not what I expect:

72|     String.repeat 3 (List.length chistes)
                        ^^^^^^^^^^^^^^^^^^^^
This call produces:

    Int

But `String.repeat` needs the 2nd argument to be:

    String

Hint: Want to convert an Int into a String? Use the String.fromInt function!

La segunda pieza merece atención especial porque encierra un problema difícil que casi todos los compiladores resuelven mal. Cuando la inferencia de tipos falla, el punto donde el algoritmo detecta la incompatibilidad casi nunca es el punto donde el programador se equivocó: la información viaja por el árbol y el conflicto aflora donde dos ramas se encuentran, que puede estar a decenas de líneas del origen. Un compilador ingenuo informa allí donde tropezó y manda al lector a un sitio inocente. Elm invierte trabajo considerable en propagar el origen de cada restricción para poder señalar la expresión que realmente introdujo el tipo conflictivo, y esa inversión, invisible en el resultado, es la diferencia entre un mensaje útil y uno que obliga a adivinar.

💡
Leer el mensaje en el orden en que está escrito

La costumbre adquirida en otros entornos consiste en saltar directamente al número de línea y volver al código, ignorando la prosa. Con Elm esa costumbre cuesta tiempo, porque el mensaje ya contiene el fragmento relevante y porque la parte que resuelve el problema suele estar en la explicación y en la pista, no en la localización. Merece la pena forzarse durante las primeras semanas a leer el texto entero de arriba abajo antes de tocar nada. Casi siempre, cuando terminas de leer, ya sabes qué escribir, y en los casos en que no lo sabes has aprendido algo del lenguaje que no habrías aprendido volviendo al editor a la primera línea.

La descendencia: qué se copió de verdad

Rust es el caso mejor documentado y el más ambicioso. A partir de 2016 su equipo de diagnósticos rehízo por completo el formato, adoptando el fragmento de código anotado con marcas de extensión, la explicación en lenguaje llano y las sugerencias accionables, y reconociendo explícitamente a Elm como referencia. Después fue más allá en dos direcciones que Elm no había recorrido: cada error tiene un código consultable con rustc --explain que despliega una explicación larga con ejemplos, y cada sugerencia lleva adjunta una anotación de fiabilidad que permite a la herramienta de arreglo automático aplicar sin preguntar solo las que son seguras. La estética de Rust ha sido a su vez copiada por bibliotecas de diagnóstico que hoy usan proyectos escritos en muchos otros lenguajes.

error[E0308]: mismatched types
  --> src/main.rs:12:20
   |
12 |     let n: usize = chistes.len() as i64;
   |            -----   ^^^^^^^^^^^^^^^^^^^^ expected `usize`, found `i64`
   |            |
   |            expected due to this
   |
help: you can convert an `i64` to a `usize` and panic if the value cannot fit
   |
12 |     let n: usize = (chistes.len() as i64).try_into().unwrap();
   |                    +                     ++++++++++++++++++++

La herencia es reconocible a simple vista y va más allá del aspecto: aparecen el fragmento anotado, la explicación del contraste entre lo esperado y lo encontrado, la referencia a la declaración que impuso la expectativa y la propuesta de arreglo escrita como código sustituible. Lo que Rust añadió por su cuenta es la trazabilidad múltiple, la capacidad de señalar dos puntos relacionados a la vez, y la conversión de la sugerencia en un dato estructurado que otra herramienta puede aplicar sin intervención humana.

TypeScript siguió un camino distinto y más limitado por su propia naturaleza. Adoptó las sugerencias de nombres parecidos, que resuelven la clase de fallo más frecuente y más trivial, e incorporó después información relacionada que permite a un mensaje apuntar a varios sitios a la vez, de modo que un conflicto entre una declaración y su uso puede mostrar ambos. Y elaboró la explicación de las incompatibilidades anidadas, descendiendo por la estructura hasta la hoja concreta donde difieren dos tipos en lugar de imprimir dos tipos enormes uno encima del otro. Es una mejora real, pero se topa con un techo que no es de esfuerzo: cuando un tipo es una unión de veinte miembros con genéricos condicionales, no hay redacción que salve el mensaje.

flowchart LR
E[Elm 2015 mensajes para humanos] --> R[Rust diagnosticos con span]
E --> T[TypeScript did you mean]
R --> C[Codigos explicables y arreglo automatico]
R --> L[Bibliotecas de diagnostico reutilizables]
T --> I[Informacion relacionada multiple]
E --> P[Python columnas precisas y sugerencias]
E --> S[Scala 3 formato con explicacion]
style E fill:#a6e3a1,color:#11111b
style R fill:#f9e2af,color:#11111b
style T fill:#89b4fa,color:#11111b

Fuera de esos dos, el patrón se ha extendido lo suficiente como para volverse invisible. Python incorporó sugerencias de nombres parecidos para atributos y variables, y después marcó con precisión de columna la subexpresión culpable dentro de una línea larga, que era exactamente el problema que resolvían los circunflejos de Elm. Scala 3 rehízo su formato de error con explicación opcional extendida. Los entornos de desarrollo web adoptaron las superposiciones de error con fragmento de código anotado. Nada de esto se atribuye ya a nadie, que es la forma que tiene una idea de haber ganado del todo.

Lo que un buen mensaje presupone del lenguaje

Aquí llega la parte incómoda, y es la que separa esta lección de un elogio. La calidad del diagnóstico de Elm no se explica solo por el esmero de quien lo escribió: se explica sobre todo por el hecho de que el compilador sabe con exactitud qué esperaba en cada punto. Y lo sabe porque el lenguaje se lo permite. No hay sobrecarga de funciones, así que no hay que enumerar candidatas ni explicar por qué ninguna encaja. No hay subtipado ni conversiones implícitas, así que no hay cadenas de coerción que reconstruir. No hay despacho dinámico, así que cada aplicación es un sitio determinado estáticamente. No hay tipos condicionales ni computación en el nivel de los tipos, así que el tipo esperado se puede imprimir tal cual y cabe en dos líneas.

🚦

Sin sobrecarga

Un nombre designa una función. No hay que enumerar candidatas ni justificar por qué ninguna encaja, que es el mensaje más ilegible que existe.

🧊

Sin coerciones

Ningún valor cambia de tipo por el camino, así que el compilador nunca tiene que explicar una conversión que el programador no escribió.

✂️

Tipos imprimibles

Sin computación en el nivel de los tipos, lo esperado se imprime literalmente. Un tipo que ocupa una pantalla no comunica nada aunque sea exacto.

🔗

Origen rastreable

La inferencia conserva de dónde vino cada restricción, y por eso el mensaje puede señalar la expresión culpable y no el punto donde tropezó.

Hay una segunda condición, menos evidente y igual de determinante: el tamaño del lenguaje. Un mensaje explicativo tiene que enumerar las posibilidades razonables de lo que el lector quiso hacer, y esa enumeración solo es viable si el número de construcciones del lenguaje es pequeño. Cuando existen quince formas de declarar algo parecido, la pista útil se vuelve imposible de redactar porque el compilador no tiene forma de saber cuál de las quince estabas intentando usar, y el texto degenera en una lista de alternativas que no ayuda a nadie. Elm cabe entero en un puñado de construcciones, y por eso la pista casi siempre acierta.

Un mensaje de error es la factura del diseño del lenguaje

La conclusión que casi nunca se extrae del éxito de Elm en este terreno es la más importante, y es una conclusión sobre diseño de lenguajes disfrazada de conclusión sobre redacción. Cuando un compilador produce un mensaje malo, la reacción natural es pensar que alguien no se esforzó lo suficiente, y a veces es cierto; pero en la mayoría de los casos el mensaje es malo porque el lenguaje ha hecho imposible uno bueno. Cada mecanismo que introduce ambigüedad en el momento de resolver un significado convierte el diagnóstico en una explicación de por qué falló una búsqueda entre alternativas, y explicar una búsqueda fallida es intrínsecamente más difícil que explicar una expectativa incumplida. La sobrecarga obliga a listar candidatas y a justificar cada descarte. El subtipado obliga a exhibir una cadena de relaciones. Las conversiones implícitas obligan a explicar un camino que el usuario nunca escribió. La inferencia bidireccional con genéricos anidados obliga a imprimir tipos que ocupan una pantalla y que, aunque son literalmente correctos, no comunican nada. Ninguno de esos mecanismos es un error de diseño en sí mismo: cada uno se introdujo para comprar expresividad, y la expresividad es real. Pero el precio se paga en una moneda que casi nunca aparece en la discusión, y es el minuto en que alguien se queda mirando una pantalla sin entender qué le está diciendo su herramienta. Elm hizo dos cosas, no una: escribió mensajes con cuidado exquisito, y antes de eso construyó un lenguaje en el que un mensaje así era posible. La segunda hazaña es la difícil, es la que no se puede añadir después y es la que ningún lenguaje maduro puede replicar, porque significaría quitar mecanismos que su ecosistema ya usa. De ahí la lección práctica, que es más profunda de lo que parece: cuando evalúes un lenguaje, provoca deliberadamente cinco errores típicos antes de escribir la primera línea útil y lee lo que te dice. Estás midiendo, sin proponértelo, cuánta ambigüedad ha aceptado ese lenguaje en su núcleo, y con ella estás midiendo cuánto tiempo de tu vida vas a pasar traduciendo a lenguaje humano lo que una máquina no supo decirte.

⚔️ Interroga a tu compilador
  1. Provoca a propósito un error de nombre mal escrito y comprueba cuántas alternativas te sugiere el compilador y con qué criterio parecen ordenadas.
  2. Escribe una incompatibilidad de tipos a tres niveles de anidamiento y localiza en el mensaje qué expresión concreta señala el circunflejo frente a dónde tú creías que estaba el fallo.
  3. Reproduce los mismos dos errores en otro lenguaje que uses a diario, guarda ambos mensajes y compáralos pieza por pieza con la anatomía de seis elementos.
  4. Toma el peor mensaje de error que hayas visto este mes y reescríbelo entero siguiendo el orden qué pasa, dónde pasa y qué hacer.
  5. Localiza en tu lenguaje habitual un mecanismo de ambigüedad, sobrecarga, coerción o subtipado, y construye el mensaje ilegible que ese mecanismo hace inevitable.
  6. Defiende la posición contraria: argumenta en qué situaciones un mensaje breve y técnico resulta preferible a uno explicativo, y para qué perfil de usuario.