JavaScript limpio no sigue una lista: deja visibles decisiones, efectos y fallos

“Usa const”, “pon nombres descriptivos”, “no hagas funciones largas”, “prefiere ===”. Todas son recomendaciones razonables. El problema aparece cuando una persona sigue varias y aún así nadie entiende el archivo dos semanas después. El código puede estar bien indentado, tener variables con nombres elegantes y pasar el linter, pero seguir escondiendo una decisión importante dentro de una función que también consulta la pantalla, modifica datos y atrapa cualquier error imaginable.

Por eso el código JavaScript limpio no debería enseñarse como una colección de mandamientos. Un fragmento es más legible cuando quien lo revisa puede localizar qué decisión toma, qué efecto produce y qué ocurre si falla sin ejecutar mentalmente toda la función. const, una función pequeña o un comentario pueden ayudar a esa meta, pero no son la meta por sí mismos.

Un botón de compra que hace cuatro trabajos sin anunciarlo

Observa una función típica de una interfaz:

async function comprar() {
  try {
    const email = document.querySelector("#email").value;
    const cantidad = Number(document.querySelector("#cantidad").value);

    if (email && cantidad > 0) {
      const respuesta = await fetch("/api/compra", {
        method: "POST",
        body: JSON.stringify({ email, cantidad })
      });

      const datos = await respuesta.json();
      document.querySelector("#mensaje").textContent = datos.ok
        ? "Compra realizada"
        : datos.error ? datos.error : "No se pudo completar";
    } else {
      document.querySelector("#mensaje").textContent = "Revisa los datos";
    }
  } catch (error) {
    console.log("Algo salió mal");
  }
}

El ejemplo no es terrible porque use let o porque tenga demasiadas líneas. Es difícil de leer porque mezcla cuatro responsabilidades: toma valores de la página, decide si son válidos, conversa con una red y modifica la interfaz. Además, atrapa cualquier problema y lo reduce a “algo salió mal”, con lo que borra información útil para una persona que intenta depurar.

Antes de cambiar la sintaxis, dibuja el recorrido:

Tipo de trabajoPregunta que respondeEjemplo en la función
Decisión¿La entrada permite continuar?email && cantidad > 0
Efecto externo¿Qué cambia fuera de la función?Enviar una solicitud y actualizar el DOM.
Transformación¿Cómo pasa un dato de una forma a otra?Number(...) y creación del cuerpo JSON.
Falla¿Qué puede no cumplirse y quién debe saberlo?Respuesta HTTP, JSON inválido, red caída.

Esta clasificación no obliga a crear una función por cada línea. Sirve para detectar por qué alguien no puede razonar sobre el código. Si una función tiene una sola frontera clara, leerla es sencillo. Si cruza muchas fronteras sin nombrarlas, el lector debe retener todo en memoria.

La limpieza empieza cuando una función hace una promesa reconocible

MDN define una función como un conjunto de instrucciones que realiza una tarea o calcula un valor, y subraya una relación identificable entre entrada y salida. Esa relación es una brújula práctica. Una función llamada validarCompra() debería responder si la compra es válida; enviarCompra() debería conversar con la API; mostrarMensaje() debería cambiar la interfaz. Cuando una sola función promete las tres cosas, su nombre empieza a mentir por omisión.

function validarCompra({ email, cantidad }) {
  if (!email.includes("@")) {
    return { valida: false, mensaje: "Escribe un correo válido" };
  }

  if (!Number.isInteger(cantidad) || cantidad <= 0) {
    return { valida: false, mensaje: "La cantidad debe ser un entero positivo" };
  }

  return { valida: true };
}

async function enviarCompra(compra) {
  const respuesta = await fetch("/api/compra", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(compra)
  });

  if (!respuesta.ok) {
    throw new Error("La compra fue rechazada por el servidor");
  }

  return respuesta.json();
}

function mostrarMensaje(texto) {
  document.querySelector("#mensaje").textContent = texto;
}

Ahora cada nombre admite una pregunta concreta. Puedes probar validarCompra() sin crear elementos HTML ni simular una API. Puedes revisar enviarCompra() y discutir qué hacer con una respuesta no satisfactoria. Puedes cambiar el diseño de la página sin tocar las reglas de validación. Separar estas piezas no es una obsesión estética: reduce la cantidad de contexto necesario para cambiar una cosa sin romper otra.

La guía de funciones de MDN también recuerda una fuente frecuente de confusión: pasar un objeto permite que cambios en sus propiedades sean visibles fuera de la función. Esto significa que los efectos no siempre se ven en un return. Si una función recibe carrito y modifica carrito.total, el lector necesita saber que esa mutación es una parte intencional de su contrato, no un detalle escondido.

const ayuda a leer intención, pero no protege tu modelo

Preferir const cuando no vas a reasignar una variable es una buena convención porque comunica que el nombre mantendrá la misma referencia. Pero no congela el objeto apuntado. Este código es válido:

const carrito = { productos: [] };
carrito.productos.push("cuaderno");

El nombre carrito no se reasignó; la lista que contiene sí cambió. Si enseñas “usa const para evitar cambios”, estás ocultando la diferencia que de verdad importa: reatribución de un nombre y mutación de un objeto. El artículo sobre var, let y const desarrolla esa distinción; aquí interesa su consecuencia para la legibilidad. Una variable declarada con const puede seguir participando en efectos importantes. El lector debe poder ver dónde y por qué ocurren.

Una práctica razonable es elegir una de estas dos rutas y nombrarla:

function agregarProducto(carrito, producto) {
  carrito.productos.push(producto);
}

function conProductoAgregado(carrito, producto) {
  return {
    ...carrito,
    productos: [...carrito.productos, producto]
  };
}

La primera muta el carrito recibido; la segunda devuelve otro objeto. Ninguna es moralmente superior en todos los casos. Lo importante es que el nombre y el lugar de la operación permitan detectar el efecto. Cuando el código oculta mutaciones detrás de funciones vagas como procesar() o actualizar(), el problema no se resuelve renombrando una variable: hay que aclarar la frontera de responsabilidad.

Los condicionales son difíciles cuando esconden un árbol de decisiones

Un ternario simple puede ser más directo que un if extenso. Pero cuando los ternarios se anidan, quien lee debe simular varias bifurcaciones en una sola línea. La regla no-nested-ternary de ESLint lo formula sin ambigüedad: los ternarios anidados dificultan la comprensión. No es que el operador sea prohibido; es que una decisión ya no cabe con claridad en esa forma.

const estado = pagado
  ? "completado"
  : cancelado
    ? "cancelado"
    : vencido
      ? "vencido"
      : "pendiente";

La versión anterior funciona, pero no cuenta la historia de la decisión. Una estructura explícita sí:

function obtenerEstado({ pagado, cancelado, vencido }) {
  if (pagado) return "completado";
  if (cancelado) return "cancelado";
  if (vencido) return "vencido";
  return "pendiente";
}

Ahora el orden de prioridad es visible. Si “cancelado” debe vencer a “pagado” por una regla de negocio, hay un lugar claro para cambiarlo y una prueba fácil de escribir. La legibilidad no significa maximizar líneas; significa minimizar decisiones invisibles.

Un catch no debería ser una aspiradora de errores

JavaScript permite lanzar y capturar excepciones, y la guía de control de flujo y manejo de errores de MDN explica que try marca el bloque que puede fallar, catch decide cómo responder y finally ejecuta limpieza incluso si hubo error. Esa separación es una oportunidad para hacer que la falla sea visible, no para enterrarla.

async function guardarCompra(compra) {
  try {
    return await enviarCompra(compra);
  } catch (error) {
    console.error("No se pudo guardar la compra", error);
    throw new Error("No pudimos registrar tu compra. Inténtalo de nuevo.");
  }
}

Este ejemplo no usa try/catch “en todo el código”. Lo coloca alrededor de una frontera que puede fallar por red o servidor, registra el detalle técnico y devuelve una consecuencia comprensible para la interfaz. Si pones un catch gigante alrededor de diez operaciones distintas, luego no sabes si el problema fue una conversión, un selector inexistente, una respuesta HTTP o una línea de renderizado.

Los errores también tienen contrato. Una función de validación puede devolver un resultado esperado; una función de red puede lanzar una excepción cuando la promesa externa no se cumple. Cuando estas decisiones se mezclan, el programa empieza a tratar fallos previsibles y defectos inesperados como si fueran lo mismo.

Esta separación se vuelve aún más importante en operaciones asíncronas. Una respuesta de red no llega en el mismo momento en que el usuario hace clic, y el estado de la interfaz puede cambiar mientras esperas. La guía sobre objetos en JavaScript ayuda a reconocer qué información viaja junta; desde ahí, puedes decidir qué función prepara los datos, cuál espera la respuesta y cuál actualiza la pantalla. Esa frontera evita que un await esconda responsabilidades que después nadie sabe dónde depurar.

Los comentarios valen cuando explican una restricción que el código no puede contar solo

“Incrementa el contador” encima de contador += 1 no ayuda a nadie. En cambio, “mantenemos el primer precio confirmado porque el proveedor puede actualizar el catálogo durante el pago” explica una decisión que no es obvia mirando la sintaxis. Un buen comentario responde por qué esta implementación existe y qué regla se perdería si alguien la simplifica.

Si necesitas comentar cada paso para que una función sea entendible, considera primero si el nombre de la función, la separación de responsabilidades o el orden de las decisiones pueden mejorar. Los comentarios no reemplazan estructura; registran contexto que la estructura no puede contener por sí sola.

Esta idea se conecta con aprender a leer errores. Cuando una función tiene una responsabilidad clara y un nombre preciso, un stack trace señala un lugar más útil. Si quieres practicar ese recorrido, la guía sobre el patrón detrás de errores comunes de JavaScript puede ayudarte a reconocer cuándo el lenguaje está interpretando algo distinto a tu intención.

Preguntas que conviene hacer al releer tu propio código

¿Cuándo debo extraer una función?

Cuando puedes nombrar un tramo por la decisión o la transformación que realiza y ese tramo exige un contexto más pequeño que el de la función actual. Extraer no es contar líneas; es reducir responsabilidades mezcladas y permitir probar o cambiar una parte sin cargar el resto.

¿Una mutación siempre es mala?

No. Puede ser la forma más directa de actualizar estado local. El criterio es que quien llama sepa que ocurrirá y que la función no mezcle esa mutación con efectos inesperados. Nombres, límites y pruebas deben hacer visible el cambio.

¿Debo envolver cada función con try/catch?

No. Colócalo en la frontera donde puedes responder de forma útil a una falla, como una petición de red o una lectura externa. Capturar indiscriminadamente puede ocultar errores de programación que necesitas ver y corregir.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio