Cada carga de PDF fallaba con el mismo ReferenceError. El stack trace no apuntaba a nada útil, y la especificación que guiaba el código parecía perfectamente razonable sobre el papel. Decía que se debía comprobar un feature flag y comparar cada región con la mitad de pageWidth. Describía lo que debía suceder, pero no describía de dónde se suponía que debía provenir pageWidth, y esa única omisión fue suficiente para tumbar todo el pipeline.

Los documentos de arquitectura son buenos explicando el comportamiento. Suelen ser pésimos explicando los límites. Una frase que dice "la función comprueba x contra pageWidth" no es un contrato técnico; es una narrativa que oculta una dependencia tras un lenguaje natural. Cuando un desarrollador lee esa frase y escribe una función a nivel de módulo que hace referencia a pageWidth por su nombre, el código parece correcto porque satisface la descripción. Entonces, el runtime intenta resolver el nombre, no encuentra nada en el scope y lanza un error.

El refactor que debería haber sido sencillo

Vi este patrón exacto durante un refactor de ensamblaje de páginas. La especificación enumeraba dos requisitos aparentemente claros:

  • Comprobar FEATURE_LAYOUT.
  • Comparar cada región con pageWidth / 2.

El desarrollador siguió las instrucciones fielmente. Extrajo una función de utilidad en el scope del módulo y colocó pageWidth directamente en el cuerpo de la función sin definirla como un parámetro. La especificación no había indicado que pageWidth debía llegar a través de una lista de argumentos. No había indicado que la función residía en el scope del módulo, donde pageWidth ya no era visible. Simplemente asumía que el implementador comprendía el contexto de ejecución de forma implícita.

El resultado fue un ReferenceError en cada carga de PDF. Debido a que la variable estaba ausente en el scope del módulo, la función fallaba inmediatamente. Si la especificación hubiera nombrado explícitamente el límite de la función y sus entradas, el desarrollador habría pasado pageWidth como argumento, y el error habría sido estructuralmente imposible. En su lugar, la instrucción actuó como una trampa, invitando al implementador a buscar en un scope padre que no existía.

Los cuatro significados de "usa"

El problema de fondo es que la prosa carece de un sistema de tipos. Cuando una especificación dice "la función usa X", la frase es ambigua de al menos cuatro formas específicas en una base de código de JavaScript moderna:

  • La función recibe X como un parámetro formal.
  • La función lee X de una variable a nivel de módulo declarada en el mismo archivo.
  • La función crea un closure sobre X desde un scope padre anidado.
  • La función extrae X de un objeto más grande que se le pasa.

Cada una de estas opciones cumple con la redacción de la especificación. Todas pasan el análisis estático. Pero solo una es correcta para un límite determinado, y la elección incorrecta filtra suposiciones a través de ese límite de una manera que compila silenciosamente.

Los desarrolladores suelen elegir el camino de menor resistencia al momento de escribir. Si pageWidth resulta estar en un scope externo, la leerán de ahí en lugar de alterar la firma de la función. Un closure oculta la dependencia. El código funciona en la primera ejecución, pasa la suite de pruebas y se despliega. Semanas después, alguien mueve esa misma función a un archivo diferente para reutilizarla o mejorar la legibilidad. El scope padre desaparece. El código se rompe, y el error parece una nueva regresión, aunque la causa raíz fuera la dependencia oculta original.

Los Web Workers borran la evidencia

Este problema se vuelve realmente perverso una vez que los Web Workers entran en la arquitectura. Cuando ocurre un error dentro de un worker, el navegador elimina la información que más necesitas.

Esto es lo que sucede realmente. Dentro de un worker, una excepción no capturada dispara un ErrorEvent. Si el worker reenvía ese error al hilo principal, el patrón típico es capturar la cadena message y enviarla a través del límite. El hilo principal recibe esa cadena, construye un nuevo objeto Error a partir de ella y la registra o la vuelve a lanzar. Lo que aparece en DevTools es el error reconstruido dentro del manejador de mensajes del hilo principal. El nombre de archivo original, el número de línea y el stack trace se descartan. La ubicación real del fallo se vuelve invisible.

Así que, cuando la ausencia de pageWidth provocó un ReferenceError dentro del worker, el hilo principal informó únicamente el texto "pageWidth is not defined" en el lugar donde se manejó el mensaje. La función real a nivel de módulo estaba en