Inicio / Artículos / ¿Por qué `catch()` lanza un SyntaxError en JavaScript?

¿Por qué `catch()` lanza un SyntaxError en JavaScript?

Aprenda por qué una lista vacía de parámetros catch interrumpe por completo el análisis de JavaScript, y conozca las dos formas correctas desde el punto de vista gramatical para escribir un bloque catch sin parámetros.

1836 palabras

A primera vista, parece que el fragmento imprimirá “Error”. En realidad, todo el archivo genera un SyntaxError, ya que escribir catch () sin nada entre paréntesis nunca ha sido JavaScript válido.

Imagínese una segunda ronda de entrevista para desarrolladores front-end. Lo que parecía ser una pregunta de calentamiento se convirtió en algo más complicado. El entrevistador pidió un ejemplo de try-catch que lanzara un error y registrara un mensaje dentro del bloque catch, y luego añadió una pista: “no necesitas el objeto de error”.

Si no se necesita el objeto de error, lo lógico es omitir su declaración. Años escribiendo cosas como function handler() {} hacen que uno acostumbre a dejar la lista de parámetros vacía:

try {
  throw "Error";
} catch () {
  console.log('Error')
}

Cuando se preguntó qué saldría de esto, la respuesta obvia parece ser "Error": se lanza la cadena, se ejecuta el bloque catch y se dispara console.log. Pero cuando el entrevistador hizo ejecutar realmente el código:

Uncaught SyntaxError: Unexpected token ')'

Nada se imprime. Ni "Error", ni nada más. La script nunca ejecuta ni una sola instrucción. Fue entonces cuando el entrevistador dijo la frase de la que toma su título este artículo:

Tienes cinco años de experiencia en JavaScript y no sabes cómo funciona el bloque try-catch.

No estaba formulado como una pregunta. Era una afirmación, y una incómoda, porque resultaba ser cierta. A lo largo de cinco años escribiendo JavaScript, este desarrollador nunca había escrito catch (). El parámetro siempre tenía un nombre, incluso en casos en los que nunca se refería a él realmente. El primer intento por omitirlo reveló una regla que simplemente nunca se había aprendido.

catch tiene exactamente dos formas válidas

La gramática formal para la cláusula catch (ECMA-262, sección 14.15, The try Statement) es concisa:

Catch :
  catch ( CatchParameter ) Block
  catch Block
CatchParameter :
  BindingIdentifier
  BindingPattern

Observe atentamente la primera regla de producción. Cuando están presentes paréntesis, un CatchParameter es obligatorio dentro de ellos. Nada en la gramática lo indica como opcional. Debe corresponderse exactamente con un único binding: un identificador simple como e, o un patrón de desestructuración como {message}. Un par de paréntesis vacíos no satisface ninguna de estas reglas.

La segunda regla de producción, introducida con ES2019, elimina los paréntesis por completo. Los paréntesis vacíos no coinciden con ninguna regla. Por lo tanto, cuando el analizador lee catch (, espera un token de binding válido a continuación, pero en su lugar encuentra ) y detiene el proceso. Ese es precisamente el mensaje que muestra V8: Unexpected token ')'.

Esto plantea una pregunta legítima: ¿por qué function f() {} se compila sin problemas mientras que catch () {} nunca lo hace? La lista de parámetros de una función sigue la gramática FormalParameters, que permite cero parámetros, valores por defecto y sintaxis de resto. CatchParameter fue diseñado intencionalmente de manera diferente: representa exactamente un enlace obligatorio, sin lista separada por comas, sin valores por defecto y sin elemento de resto. Catch no tiene concepto de lista de parámetros vacía, por lo que el atajo mental de “simplemente dejar los paréntesis vacíos”, que funciona para las funciones, no es aplicable aquí.

Este error ocurre antes de que se ejecute tu código

Había un segundo concepto erróneo oculto en este error: tratar los errores como un fenómeno puramente de tiempo de ejecución. Este fallo en particular ocurre durante el análisis sintáctico, antes incluso de que comience la ejecución. El motor escanea todo el script, no logra coincidirlo con la gramática y reporta un SyntaxError antes de que tenga oportunidad de ejecutarse siquiera una línea. Todo lo demás en ese archivo se ve afectado negativamente.

Agregar otra línea lo hace evidente. Lo siguiente se confirmó en Chrome:

console.log('before');            // NEVER runs
try { throw "Error"; } catch() { }
Uncaught SyntaxError: Unexpected token ')'

La línea console.log('before') se encuentra por encima de la cláusula catch mal formada y, por sí sola, no tiene ningún problema, sin embargo tampoco se imprime nunca. Todo el script falla al compilarse, por lo que nada de él se ejecuta.

Esto conduce a una conclusión que es fácil pasar por alto al principio: un bloque try-catch dentro de un archivo no puede capturar un SyntaxError que provenga de ese mismo archivo. Para que cualquier bloque catch se ejecute, el código circundante ya debería haberse analizado con éxito, lo cual es precisamente lo que falló. La única forma de capturar este tipo de error es desde una unidad de compilación separada, mediante eval, new Function o un import() dinámico.

Las dos formas de escribirlo correctamente

Ambos enfoques mencionados a continuación fueron verificados en Chrome, y ambos son válidos.

Opción 1: mantener el parámetro, pero no usarlo. Declinar un parámetro de catch al que nunca se hace referencia es completamente legal, y siempre lo ha sido, en todas las versiones de ECMAScript.

try {
  throw "Error";
} catch (e) {
  console.log('caught, e unused');
}
// logs: caught, e unused

Opción 2: eliminar por completo los paréntesis. Esta es la sintaxis de enlace opcional catch de ES2019: sin paréntesis, sin parámetro, simplemente catch {.

try {
  throw "Error";
} catch {
  console.log('caught without binding');
}
// logs: caught without binding

El modelo mental a mantener: acortar catch (e) significa eliminar los paréntesis, no quitar lo que hay dentro de ellos. El punto intermedio entre estas dos formas válidas es exactamente donde se produce el SyntaxError.

Origen de la sintaxis catch sin paréntesis

El enlace opcional catch surgió como una propuesta del TC39 redactada por Michael Ficarra. Llegó a la fase 4 en enero de 2018 y pasó a formar parte de ES2019. El soporte en navegadores y entornos de ejecución llegó rápidamente: Chrome 66, Firefox 58, Safari 11.1 y Node.js 10 lo soportan todos, lo que significa que es seguro usarlo en prácticamente cualquier entorno al que se pueda desplegar hoy en día.

La lógica detrás de la propuesta coincide con el escenario exacto que causó problemas al candidato en la entrevista: código en el que el error detectado realmente nunca es necesario. Piense en verificar si una cadena se puede parsear como JSON válido y recurrir a un valor por defecto cuando no es posible, o en patrones de detección de funcionalidades donde simplemente capturar el error ya le proporciona toda la información necesaria. En casos como estos, nombrar el error como e solo crea una variable que se asigna pero nunca se lee; además, la propuesta misma señala que este patrón suele indicar un error en otra parte del código.

Vale la pena ser preciso sobre qué cambió realmente la propuesta: introdujo una nueva producción gramatical para un bloque catch sin ninguna lista de parámetros. No legalizó los paréntesis vacíos; los paréntesis no se dejan vacíos, sino que se eliminan por completo. Esto mantiene la regla de que CatchParameter, cuando está presente, debe ser exactamente un enlace, y hace que catch { sea visualmente consistente con finally {, un bloque que nunca tuvo paréntesis en primer lugar.

Por qué incluso los desarrolladores experimentados caen en esto

Hay tres razones separadas por las que esto confunde a la gente, y ninguna de ellas se debe a una falta de esfuerzo o estudio.

Tus instintos en este caso provienen de un patrón diferente y más familiar, y te engañan. Cada firma de función que hayas escrito refuerza la idea de que una lista de parámetros no utilizados puede reducirse a paréntesis vacíos: function () {} es algo completamente normal. Dado que una cláusula catch se asemeja visualmente al encabezado de una función, aplicar el mismo atajo parece natural. Pero una cláusula catch no acepta una lista de parámetros, sino un único valor de enlace, y la gramática simplemente nunca ha incluido una variante vacía de ella.

Este error generalmente no se manifiesta hasta el momento exacto en que intentas eliminar un e no utilizado. Ese momento suele ser desencadenado por una queja del linter. La regla no-unused-vars de ESLint incluye una opción caughtErrors, y a partir de ESLint 9, esa opción tiene como valor predeterminado "all" — lo que significa que un catch (e) no utilizado genera automáticamente un error de lint. Al intentar solucionar esa advertencia, los desarrolladores vacían instintivamente los paréntesis de la misma manera que lo harían en una firma de función, y ahí es precisamente donde el analizador los detiene. La solución realmente correcta —eliminar completamente los paréntesis y escribir catch {— es la que casi nadie elige primero, porque en ningún otro lugar de JavaScript “acortar” significa eliminar en lugar de vaciar.

El error nunca dura lo suficiente como para dejar una marca. Un error lógico sutil puede colarse en el entorno de producción y perseguir a un código durante meses, convirtiéndose en ese tipo de historia que los desarrolladores repiten durante años. Este no es nada parecido: se trata de un SyntaxError detectado inmediatamente al momento del análisis. Ves la línea ondulada debajo del texto, lo arreglas en segundos y sigues adelante sin pensarlo más. No queda ningún recuerdo duradero de él, y precisamente por eso es una pregunta efectiva en las entrevistas. Examina la diferencia entre la sintaxis que realmente has asimilado y la sintaxis que solo supones que entiendes.

Una advertencia antes de que comience a reescribir cada catch (e) que encuentre: catch { } le permite omitir el *nombramiento* del error, pero no omitir su *manejo*. La forma de ES2019 existe para una lógica legítima de prueba y solución alternativa en la que el mero hecho de capturar el error ya constituye la señal útil. Un cuerpo de catch vacío que ignora silenciosamente un fallo real sigue siendo tan problemático como siempre, con o sin paréntesis.

La respuesta que habría sido más adecuada

La respuesta que habría mantenido esta entrevista en el camino correcto: “Esto genera un SyntaxError al momento del análisis, específicamente Unexpected token ')'. Nada en el archivo se ejecuta en absoluto, ni siquiera el código que aparece antes del bloque try. Una cláusula catch solo tiene dos formas válidas: catch (binding) { }, o, desde ES2019, la versión sin parámetros catch { }. Los paréntesis vacíos no coinciden con ninguno de estos patrones.”

Puntos clave que vale la pena recordar:

  • catch () con paréntesis vacíos siempre ha sido, y sigue siendo, un SyntaxError en todas las versiones de JavaScript.
  • Existen exactamente dos formas válidas: catch (e) { } con un único parámetro nombrado, o catch { } sin paréntesis en absoluto, introducida en ES2019.
  • CatchParameter es un único vínculo obligatorio, no una lista de parámetros; la costumbre de usar “paréntesis vacíos” en la sintaxis de funciones simplemente no se transfiere aquí.
  • Dado que se trata de un error en el momento del análisis, todo el archivo deja de funcionar; un bloque try en otra parte del mismo archivo nunca podrá interceptarlo.
  • Dejar catch (e) con un e no utilizado es técnicamente aceptable, pero ESLint 9 lo marca por defecto con la regla no-unused-vars; la solución correcta es eliminar los paréntesis, no vaciar su contenido.
  • catch { }, que no acepta parámetros, existe para situaciones en las que el valor del error es realmente innecesario; no constituye una excusa general para ignorar silenciosamente los fallos reales.
  • ¿Fue justa la dura reacción del entrevistador? Solo en parte. El comportamiento en tiempo de ejecución de try-catch nunca estuvo en duda; esa parte era algo natural. Lo que realmente nunca se había examinado era la gramática en sí, simplemente porque el código de uso diario rara vez obliga a lidiar con ella. Hoy en día, la costumbre ha cambiado: al eliminar un e que no se utiliza, ahora significa borrar los paréntesis junto con él, y no simplemente dejarlos vacíos.

    Al acortar una cláusula catch, borre los paréntesis; nunca solo déjelos vacíos.

    Lecturas relacionadas

  • React Components 101: Construyendo elementos UI reutilizables y mantenibles — Aprenda por qué dividir las interfaces de usuario en pequeños componentes React mejora la reutilización, la legibilidad y la colaboración en equipo, y luego cree su primer componente funcional.