JavaScript a través de ejemplos que puedes ejecutar hoy
JavaScript es el único lenguaje que se ejecuta de forma nativa en todos los navegadores y, gracias a entornos como Node.js, Deno y Bun, también en servidores, herramientas de línea de comandos y funciones en la nube. Esa omnipresencia tiene un costo: en internet conviven explicaciones escritas para versiones de hace quince años con otras que usan las características más recientes. Un principiante puede terminar aprendiendo var, el objeto arguments y la simulación de clases con prototipos antes de descubrir que existen alternativas más claras y seguras.
Esta guía recorre los conceptos fundamentales del lenguaje con ejemplos propios y actualizados. Cada sección explica qué hace una construcción, cuándo conviene usarla y qué alternativa moderna la ha sustituido cuando corresponde. El escenario que acompaña los ejemplos es el inventario de una tienda de bicicletas llamada Rueda Libre, con productos, clientes, pedidos y un formulario de contacto.
El objetivo es que entiendas el JavaScript moderno con ejemplos prácticos, desde los operadores básicos hasta las clases con campos privados, la programación asíncrona y las novedades incorporadas en ECMAScript 2026, la edición más reciente del estándar, aprobada en junio de 2026.
Dónde ejecutar los ejemplos
No necesitas instalar nada para empezar. Todos los navegadores modernos incluyen una consola de JavaScript: abre las herramientas de desarrollo con F12 o con la combinación Ctrl+Shift+J en Windows y Linux, o Cmd+Option+J en macOS, y escribe directamente.
Para ejecutar archivos fuera del navegador, Node.js es la opción más extendida. En este momento la versión 26 es la rama actual y está en proceso de convertirse en la nueva versión de soporte extendido, mientras que la 24 sigue recibiendo parches de seguridad. Comprueba tu versión e instala desde el sitio oficial o con un gestor como fnm o nvm:
node --version
node ejemplos.js
Para los ejemplos de esta guía conviene una versión 24 o superior, que ya incluye soporte nativo para la mayoría de las características descritas.
Una estructura mínima para seguir la guía puede ser esta:
rueda-libre/
├── package.json
├── index.html
└── src/
├── operadores.js
├── funciones.js
├── clases.js
├── asincronia.js
└── formulario.js
Añade "type": "module" en tu package.json para usar la sintaxis import y export de forma nativa en Node.js:
{
"name": "rueda-libre",
"type": "module"
}
Comentarios: documentar sin estorbar
Los comentarios no se ejecutan; sirven para explicar intenciones al resto del equipo o a tu yo del futuro. JavaScript tiene comentarios de una línea, que empiezan con //, y comentarios de bloque, delimitados por /* y */.
// Precio base sin impuestos
const precioBase = 489.9;
/*
Se aplica el descuento de temporada solo a
bicicletas de montaña con más de un año en stock.
*/
const descuentoTemporada = 0.15;
Existe además el formato JSDoc, que los editores entienden para mostrar ayuda contextual y detectar errores de tipos incluso sin TypeScript:
/**
* Calcula el precio final con impuestos.
* @param {number} precio - Precio sin impuestos.
* @param {number} [iva=0.21] - Tasa de impuesto.
* @returns {number}
*/
function precioFinal(precio, iva = 0.21) {
return precio * (1 + iva);
}
Un buen comentario explica por qué se hace algo, no qué hace cada línea. Si necesitas comentar cada instrucción, probablemente los nombres de variables y funciones pueden mejorar. En la mayoría de los editores, Ctrl+/ o Cmd+/ comenta y descomenta la línea seleccionada, útil para desactivar código temporalmente mientras depuras.
Operadores aritméticos y la trampa de la coerción
JavaScript incluye los operadores habituales: suma +, resta -, multiplicación *, división /, resto % y potencia **.
const ruedas = 2;
const bicis = 14;
console.log(ruedas * bicis); // 28
console.log(17 % 5); // 2
console.log(2 ** 10); // 1024
console.log(10 / 4); // 2.5
El operador + tiene una particularidad que causa muchos errores: si alguno de los operandos es una cadena, concatena en lugar de sumar. Los demás operadores aritméticos, en cambio, intentan convertir los valores a número:
console.log(5 + "5"); // '55'
console.log(5 - "2"); // 3
console.log("8" * "2"); // 16
console.log(5 - "pedal"); // NaN
Este comportamiento, llamado coerción de tipos, aparece con frecuencia al leer valores de formularios, que siempre llegan como texto. La solución es convertir explícitamente con Number() antes de operar:
const cantidad = document.querySelector("#cantidad").value; // '3'
const total = Number(cantidad) * 489.9;
Los operadores de incremento ++ y decremento -- tienen dos formas. En la forma prefija, el valor cambia antes de usarse; en la postfija, después:
let stock = 5;
console.log(stock++); // 5, luego stock vale 6
console.log(++stock); // 7
Por claridad, muchas guías de estilo recomiendan usar stock += 1 y evitar mezclar incrementos dentro de otras expresiones.
Los números en JavaScript son de coma flotante, por lo que 0.1 + 0.2 no da exactamente 0.3. Para dinero, trabaja con céntimos enteros o con Intl.NumberFormat al mostrar resultados. ES2026 añade Math.sumPrecise, que suma colecciones de números con menor pérdida de precisión:
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(Math.sumPrecise([0.1, 0.2])); // 0.3
Operadores de asignación
El operador = asigna un valor. Las formas compuestas combinan una operación con la asignación:
let saldo = 1000;
saldo += 250; // 1250
saldo -= 100; // 1150
saldo *= 2; // 2300
saldo /= 4; // 575
saldo **= 2; // 330625
Más útiles en el día a día son los operadores lógicos de asignación, incorporados en ES2021:
const pedido = { cupon: null, envio: "", notas: "Entregar por la tarde" };
pedido.cupon ??= "SIN-CUPON"; // asigna solo si es null o undefined
pedido.envio ||= "estandar"; // asigna si es falsy, incluido ''
pedido.notas &&= pedido.notas.trim(); // asigna solo si ya tiene valor truthy
La desestructuración es otra forma de asignación que extrae valores de arreglos y objetos en una sola línea:
const bici = { modelo: "Sierra 29", talla: "M", precio: 1290 };
const { modelo, precio: precioVenta } = bici;
const [primera, , tercera] = ["urbana", "carretera", "gravel"];
Comparaciones: igualdad estricta frente a laxa
JavaScript tiene dos operadores de igualdad. El doble igual == convierte los tipos antes de comparar, lo que produce resultados difíciles de recordar. El triple igual === compara valor y tipo sin conversiones.
console.log(0 == ""); // true
console.log(0 === ""); // false
console.log(null == undefined); // true
console.log(null === undefined); // false
console.log("7" == 7); // true
console.log("7" === 7); // false
La recomendación generalizada es usar siempre === y !==. La única excepción aceptada en algunos equipos es valor == null, que comprueba a la vez null y undefined.
Los operadores relacionales <, >, <= y >= comparan números de forma numérica, pero cadenas de forma lexicográfica, lo que puede sorprender:
console.log(10 < 9); // false
console.log("10" < "9"); // true, compara carácter a carácter
Para ordenar textos en español con tildes y eñes de forma correcta, usa localeCompare o Intl.Collator en lugar de los operadores:
const marcas = ["Ñandú", "Orbea", "Álamo", "Bianchi"];
marcas.sort((a, b) => a.localeCompare(b, "es"));
// ['Álamo', 'Bianchi', 'Ñandú', 'Orbea']
Booleanos, valores truthy y falsy
Cualquier valor de JavaScript puede evaluarse en un contexto booleano. Conocer los valores truthy y falsy en JavaScript evita condiciones que fallan de forma silenciosa. Solo existen unos pocos valores falsy:
false0,-0y0n- La cadena vacía
'' nullundefinedNaN
Todo lo demás es truthy, incluidos los arreglos vacíos [], los objetos vacíos {} y la cadena '0'. Esto permite escribir comprobaciones compactas, pero también introduce errores:
function mostrarStock(unidades) {
if (!unidades) {
return "Sin información";
}
return `${unidades} disponibles`;
}
console.log(mostrarStock(0)); // 'Sin información', aunque 0 es un dato válido
Aquí entra el operador de fusión nula ??, que solo recurre al valor por defecto ante null o undefined, no ante 0 o '':
const unidades = 0;
console.log(unidades || "Sin datos"); // 'Sin datos'
console.log(unidades ?? "Sin datos"); // 0
Los operadores && y || no devuelven necesariamente true o false, sino uno de sus operandos. a || b devuelve a si es truthy y b en caso contrario; a && b devuelve a si es falsy y b en caso contrario. El encadenamiento opcional ?. complementa estas herramientas al acceder a propiedades que pueden no existir:
const cliente = { nombre: "Lucía", direccion: null };
console.log(cliente.direccion?.ciudad ?? "Ciudad no indicada");
Para convertir explícitamente un valor a booleano, usa Boolean(valor) o la doble negación !!valor. Evita el constructor new Boolean(false), que crea un objeto y, por tanto, es truthy.
Condicionales: if, switch y el operador ternario
La sentencia if ejecuta un bloque cuando la condición es truthy, y puede encadenarse con else if y else:
function categoriaEnvio(pesoKg) {
if (pesoKg <= 5) {
return "paquete";
} else if (pesoKg <= 20) {
return "bulto";
} else {
return "carga";
}
}
Para asignaciones simples, el operador ternario es más conciso:
const etiqueta = stock > 0 ? "Disponible" : "Agotado";
Cuando comparas una misma variable contra muchos valores, switch resulta más legible. Recuerda el break en cada caso para evitar que la ejecución continúe en el siguiente:
function nombreTalla(codigo) {
switch (codigo) {
case "S":
return "Pequeña";
case "M":
return "Mediana";
case "L":
case "XL":
return "Grande";
default:
return "Talla desconocida";
}
}
Una alternativa frecuente al switch es un objeto de búsqueda, más fácil de extender:
const tallas = { S: "Pequeña", M: "Mediana", L: "Grande", XL: "Grande" };
const nombre = tallas[codigo] ?? "Talla desconocida";
Las cláusulas de guarda, que devuelven pronto ante casos especiales, reducen el anidamiento y mejoran la legibilidad de funciones largas.
Bucles y control de flujo
JavaScript ofrece varios bucles, cada uno pensado para un caso. El for clásico controla un contador:
for (let i = 0; i < 3; i++) {
console.log(`Revisión ${i + 1}`);
}
while repite mientras se cumple una condición, y do...while garantiza al menos una ejecución:
let intentos = 0;
do {
intentos++;
} while (intentos < 3);
Para recorrer colecciones, for...of es la opción más clara. Funciona con arreglos, cadenas, Map, Set y cualquier iterable:
const inventario = new Map([
["Sierra 29", 4],
["Urbana Lite", 0],
["Gravel Pro", 7],
]);
for (const [modelo, unidades] of inventario) {
if (unidades === 0) continue;
console.log(`${modelo}: ${unidades}`);
}
for...in, en cambio, recorre las claves enumerables de un objeto, incluidas las heredadas. No debe usarse con arreglos, porque devuelve índices como cadenas y puede incluir propiedades inesperadas. Para objetos, es más seguro iterar sobre Object.entries:
const ficha = { marca: "Orbea", cuadro: "aluminio", velocidades: 12 };
for (const [clave, valor] of Object.entries(ficha)) {
console.log(`${clave}: ${valor}`);
}
break sale del bucle por completo y continue salta a la siguiente iteración. En muchos casos, los métodos de arreglos como map, filter, reduce, find y some expresan mejor la intención que un bucle manual:
const bicis = [
{ modelo: "Sierra 29", precio: 1290, tipo: "montaña" },
{ modelo: "Urbana Lite", precio: 540, tipo: "urbana" },
{ modelo: "Gravel Pro", precio: 1850, tipo: "gravel" },
];
const asequibles = bicis.filter((b) => b.precio < 1500).map((b) => b.modelo);
const valorTotal = bicis.reduce((suma, b) => suma + b.precio, 0);
const porTipo = Object.groupBy(bicis, (b) => b.tipo);
Object.groupBy, añadido en ES2024, agrupa elementos sin necesidad de librerías externas como Lodash.
Funciones declaradas, expresiones y funciones flecha
Las funciones se pueden declarar de varias formas. La declaración clásica se eleva, es decir, puede llamarse antes de aparecer en el código:
console.log(aplicarDescuento(100, 0.1)); // 90
function aplicarDescuento(precio, tasa) {
return precio * (1 - tasa);
}
Las funciones flecha en JavaScript ofrecen una sintaxis más corta, especialmente útil en callbacks. Si el cuerpo es una sola expresión, el return es implícito:
const conIva = (precio) => precio * 1.21;
const crearPedido = (id, total) => ({ id, total, estado: "pendiente" });
Observa los paréntesis alrededor del objeto en el segundo ejemplo: sin ellos, las llaves se interpretarían como el cuerpo de la función.
La diferencia más importante entre ambas formas es el valor de this. Una función tradicional define su propio this según cómo se llama; una función flecha lo hereda del contexto donde se definió. Esto resuelve un problema clásico con temporizadores y callbacks:
class Cronometro {
segundos = 0;
iniciar() {
setInterval(() => {
this.segundos++; // this es la instancia del cronómetro
}, 1000);
}
}
Precisamente por esa herencia, las funciones flecha no son adecuadas como métodos de objetos literales que dependan de this, ni pueden usarse como constructores con new.
Del objeto arguments a los parámetros rest
Históricamente, las funciones accedían a todos sus argumentos mediante el objeto arguments, que parece un arreglo pero no lo es: no tiene métodos como map o reduce. Había que convertirlo con Array.from(arguments) o con trucos como Array.prototype.slice.call(arguments).
// Estilo antiguo
function sumarAntiguo() {
return Array.from(arguments).reduce((a, b) => a + b, 0);
}
Hoy la forma recomendada son los parámetros rest en JavaScript, que recogen los argumentos restantes en un arreglo real:
function sumarPrecios(...precios) {
return precios.reduce((total, p) => total + p, 0);
}
console.log(sumarPrecios(540, 1290, 1850)); // 3680
function registrarVenta(vendedor, ...productos) {
console.log(`${vendedor} vendió ${productos.length} artículos`);
}
Además, arguments no existe dentro de funciones flecha, así que los parámetros rest son la única opción en ese caso. El operador de propagación, con la misma sintaxis de tres puntos, hace lo contrario: expande un arreglo en argumentos individuales.
const precios = [540, 1290, 1850];
console.log(Math.max(...precios)); // 1850
Combínalos con valores por defecto para definir funciones flexibles y autoexplicativas:
function crearCliente({ nombre, email, boletin = false } = {}) {
return { nombre, email, boletin };
}
Ámbito y cierres
El ámbito determina desde dónde es visible una variable. Una creencia extendida en material antiguo es que en JavaScript solo las funciones crean ámbito. Eso era cierto con var, pero let y const, disponibles desde 2015, tienen ámbito de bloque: cualquier par de llaves crea un nuevo ámbito.
if (true) {
var global = "visible fuera";
let local = "solo dentro";
}
console.log(global); // 'visible fuera'
console.log(local); // ReferenceError
La recomendación actual es usar const por defecto, let cuando el valor deba reasignarse y evitar var.
Un cierre, o closure, se produce cuando una función recuerda las variables del ámbito donde se creó, incluso después de que ese ámbito haya terminado de ejecutarse. Los closures siguen siendo una herramienta muy útil para encapsular estado:
function crearContadorVisitas(tienda) {
let visitas = 0;
return {
registrar() {
visitas++;
return `${tienda}: ${visitas} visitas`;
},
total: () => visitas,
};
}
const contador = crearContadorVisitas("Rueda Libre");
contador.registrar();
contador.registrar();
console.log(contador.total()); // 2
console.log(contador.visitas); // undefined, la variable es privada
Antes de existir los módulos, este principio se usaba en el patrón módulo con funciones autoejecutables para no contaminar el ámbito global. Hoy los módulos ES resuelven ese problema de forma nativa: cada archivo tiene su propio ámbito y solo expone lo que exporta.
// src/carrito.js
const productos = [];
export function agregar(producto) {
productos.push(producto);
}
export function total() {
return productos.reduce((suma, p) => suma + p.precio, 0);
}
// src/main.js
import { agregar, total } from "./carrito.js";
agregar({ modelo: "Urbana Lite", precio: 540 });
console.log(total());
Un clásico problema de cierres aparecía al crear funciones dentro de un bucle con var: todas compartían la misma variable y mostraban el último valor. Con let, cada iteración tiene su propia copia, por lo que el problema desaparece sin necesidad de trucos.
Prototipos y clases
JavaScript es un lenguaje basado en prototipos: cada objeto tiene un enlace interno a otro objeto del que hereda propiedades. Cuando accedes a una propiedad que no existe en el objeto, el motor la busca a lo largo de esa cadena.
const vehiculo = {
describir() {
return `${this.nombre} con ${this.ruedas} ruedas`;
},
};
const tandem = Object.create(vehiculo);
tandem.nombre = "Tándem Duo";
tandem.ruedas = 2;
console.log(tandem.describir());
console.log(Object.getPrototypeOf(tandem) === vehiculo); // true
La palabra clave class, introducida en ES2015, ofrece una sintaxis más familiar sobre ese mismo mecanismo. Las versiones posteriores añadieron campos de instancia, campos estáticos y, sobre todo, campos y métodos privados con el prefijo #:
class Bicicleta {
static #contador = 0;
#precio;
constructor(modelo, precio) {
this.modelo = modelo;
this.#precio = precio;
Bicicleta.#contador++;
}
get precio() {
return this.#precio;
}
set precio(nuevo) {
if (nuevo <= 0) throw new RangeError("El precio debe ser positivo");
this.#precio = nuevo;
}
static get totalCreadas() {
return Bicicleta.#contador;
}
}
class Electrica extends Bicicleta {
constructor(modelo, precio, autonomiaKm) {
super(modelo, precio);
this.autonomiaKm = autonomiaKm;
}
describir() {
return `${this.modelo} (${this.autonomiaKm} km de autonomía)`;
}
}
const ebike = new Electrica("Volta 500", 2490, 90);
console.log(ebike.precio); // 2490
console.log(ebike.describir());
console.log(Bicicleta.totalCreadas); // 1
Los campos privados sustituyen técnicas antiguas como ocultar datos con Symbol o con cierres. A diferencia de esas técnicas, la privacidad con # está garantizada por el lenguaje: intentar acceder a ebike.#precio desde fuera produce un error de sintaxis. Además, Symbol no es un constructor, por lo que new Symbol() lanza un error; se llama simplemente como Symbol('descripcion').
Callbacks, promesas y async/await
JavaScript ejecuta el código en un solo hilo, pero muchas operaciones, como peticiones de red o temporizadores, tardan en completarse. Para no bloquear la ejecución, se usan mecanismos asíncronos. El más antiguo es el callback: una función que se pasa como argumento para que se ejecute cuando la operación termine.
function consultarStock(modelo, alTerminar) {
setTimeout(() => {
alTerminar(null, { modelo, unidades: 4 });
}, 500);
}
consultarStock("Sierra 29", (error, datos) => {
if (error) return console.error(error);
console.log(datos.unidades);
});
Cuando varias operaciones dependen unas de otras, los callbacks se anidan y forman la llamada pirámide de la perdición, difícil de leer y de gestionar errores. Las promesas y la sintaxis async/await resolvieron ese problema. Con async y await en JavaScript, el código asíncrono se lee casi como si fuera secuencial:
async function cargarCatalogo() {
try {
const respuesta = await fetch("/api/bicicletas");
if (!respuesta.ok) throw new Error(`HTTP ${respuesta.status}`);
const bicis = await respuesta.json();
return bicis.filter((b) => b.unidades > 0);
} catch (error) {
console.error("No se pudo cargar el catálogo:", error);
return [];
}
}
Para lanzar varias peticiones en paralelo, usa Promise.all o Promise.allSettled en lugar de esperar una tras otra:
const [bicis, accesorios] = await Promise.all([
fetch("/api/bicicletas").then((r) => r.json()),
fetch("/api/accesorios").then((r) => r.json()),
]);
ES2026 incorpora Array.fromAsync, que construye un arreglo a partir de un iterable asíncrono, por ejemplo una API paginada:
async function* paginasDePedidos() {
for (let pagina = 1; pagina <= 3; pagina++) {
const r = await fetch(`/api/pedidos?pagina=${pagina}`);
yield await r.json();
}
}
const todasLasPaginas = await Array.fromAsync(paginasDePedidos());
En módulos ES puedes usar await en el nivel superior, sin envolverlo en una función, lo que simplifica scripts y configuración inicial.
Validación de formularios
Un caso práctico que combina varios de estos conceptos es validar un formulario de contacto. El primer nivel de validación debería ser HTML: atributos como required, type="email", minlength, maxlength y pattern activan la validación nativa del navegador sin JavaScript.
<form id="contacto" novalidate>
<label for="nombre">Nombre</label>
<input id="nombre" name="nombre" required minlength="2" />
<label for="email">Correo</label>
<input id="email" name="email" type="email" required />
<label for="telefono">Teléfono</label>
<input id="telefono" name="telefono" pattern="[0-9]{9}" />
<p id="errores" role="alert"></p>
<button>Enviar</button>
</form>
Con JavaScript puedes aprovechar la API de validación de restricciones del navegador para mostrar mensajes propios y en español:
const formulario = document.querySelector("#contacto");
const zonaErrores = document.querySelector("#errores");
formulario.addEventListener("submit", async (evento) => {
evento.preventDefault();
const mensajes = [];
for (const campo of formulario.elements) {
if (!campo.name || campo.validity.valid) continue;
if (campo.validity.valueMissing)
mensajes.push(`El campo ${campo.name} es obligatorio`);
else if (campo.validity.typeMismatch)
mensajes.push("El correo no tiene un formato válido");
else if (campo.validity.patternMismatch)
mensajes.push("El teléfono debe tener 9 dígitos");
else if (campo.validity.tooShort)
mensajes.push(`El campo ${campo.name} es demasiado corto`);
}
if (mensajes.length) {
zonaErrores.textContent = mensajes.join(". ");
return;
}
const datos = Object.fromEntries(new FormData(formulario));
await fetch("/api/contacto", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(datos),
});
zonaErrores.textContent = "";
formulario.reset();
});
El atributo novalidate desactiva los globos de error nativos para que puedas mostrar los tuyos, mientras que role="alert" hace que los lectores de pantalla anuncien los mensajes. Usar textContent en lugar de innerHTML evita inyectar HTML no deseado. Recuerda que la validación del cliente mejora la experiencia, pero el servidor siempre debe volver a validar los datos.
Novedades de ECMAScript 2026
La edición 2026 del estándar, aprobada el 30 de junio de 2026, incorpora varias utilidades que sustituyen soluciones manuales habituales. Algunas ya aparecieron en esta guía; estas son las principales:
Math.sumPrecisepara sumar con menor error de redondeo.Array.fromAsyncpara construir arreglos desde fuentes asíncronas.Iterator.concatpara encadenar iteradores.Error.isErrorpara identificar errores de forma fiable.- Métodos en
MapyWeakMapque devuelven un valor por defecto cuando falta una clave. - Conversión de
Uint8Arraydesde y hacia hexadecimal y base64. - Mejoras en
JSON.parseyJSON.rawJSONpara controlar la serialización.
const bytes = new Uint8Array([72, 111, 108, 97]);
console.log(bytes.toBase64()); // 'SG9sYQ=='
console.log(bytes.toHex()); // '486f6c61'
const todos = Iterator.concat(
["urbana", "gravel"].values(),
new Set(["bmx"]).values()
);
console.log([...todos]); // ['urbana', 'gravel', 'bmx']
console.log(Error.isError(new TypeError("x"))); // true
Antes de usar estas características en producción, verifica el soporte en los navegadores y en la versión de Node.js que utilizas; si necesitas compatibilidad amplia, un transpilador o un polyfill puede cubrir la diferencia.
Errores comunes en JavaScript
Estos fallos aparecen una y otra vez, incluso en desarrolladores con experiencia:
- Comparar con
==y obtener coincidencias inesperadas por coerción. - Sumar valores de formularios sin convertirlos a número.
- Usar
||para valores por defecto cuando0o''son datos válidos. - Recorrer arreglos con
for...in. - Olvidar
awaity trabajar con una promesa en lugar de su resultado. - Usar funciones flecha como métodos que dependen de
this. - Declarar variables con
vary sufrir problemas de ámbito. - Mutar arreglos con
sortoreversesin darse cuenta; usatoSortedytoReversedpara obtener copias. - Asignar HTML de usuarios con
innerHTML, abriendo la puerta a ataques XSS.
El olvido de await merece un ejemplo, porque no lanza ningún error:
async function obtenerTotal() {
return 3680;
}
const total = obtenerTotal();
console.log(total + 100); // '[object Promise]100'
const totalCorrecto = await obtenerTotal();
console.log(totalCorrecto + 100); // 3780
Solución de problemas frecuentes
Los mensajes de error de JavaScript son más informativos de lo que parecen. Estos son los más comunes y su causa habitual.
Acceder a una propiedad de algo inexistente:
Uncaught TypeError: Cannot read properties of undefined (reading 'precio')
La variable no contiene lo que esperabas. Comprueba el origen de los datos o usa encadenamiento opcional: producto?.precio.
Usar una variable antes de declararla con let o const:
Uncaught ReferenceError: Cannot access 'descuento' before initialization
Las variables con let y const existen en una zona muerta temporal hasta su declaración. Mueve la declaración antes del uso.
Llamar a algo que no es una función:
Uncaught TypeError: bicis.map is not a function
Normalmente bicis no es un arreglo, por ejemplo porque la API devolvió un objeto como { datos: [...] }. Inspecciona la respuesta con console.log o console.table.
Usar await fuera de una función asíncrona o de un módulo:
SyntaxError: await is only valid in async functions and the top level bodies of modules
Marca la función con async o asegúrate de que el archivo se carga como módulo, con type="module" en el navegador o en tu package.json.
Usar import en Node.js sin configurar módulos:
SyntaxError: Cannot use import statement outside a module
Añade "type": "module" al package.json o usa la extensión .mjs.
Para depurar, la instrucción debugger; detiene la ejecución en las herramientas del navegador, donde puedes inspeccionar variables paso a paso, mucho más eficaz que llenar el código de console.log.
Buenas prácticas para código mantenible
Más allá de la sintaxis, ciertos hábitos hacen que tu código sea más robusto y fácil de mantener:
- Usa
constpor defecto yletsolo cuando necesites reasignar. - Compara siempre con
===y convierte tipos de forma explícita. - Prefiere
??y?.para gestionar valores ausentes. - Organiza el código en módulos ES pequeños con responsabilidades claras.
- Usa campos privados
#para proteger el estado interno de las clases. - Prefiere métodos de arreglos inmutables como
toSorted,mapyfilter. - Gestiona los errores asíncronos con
try...catchy compruebaresponse.okenfetch. - Configura un linter como ESLint y un formateador como Prettier o Biome.
- Considera JSDoc o TypeScript para detectar errores de tipos antes de ejecutar.
Un archivo de configuración mínimo para ESLint en formato plano, el estándar actual:
// eslint.config.js
import js from "@eslint/js";
export default [
js.configs.recommended,
{
rules: {
eqeqeq: "error",
"no-var": "error",
"prefer-const": "warn",
},
},
];
Conclusiones
JavaScript ha evolucionado de un lenguaje de scripts sencillo a una plataforma completa, y esa evolución se nota en cada uno de sus fundamentos. Los operadores siguen comportándose como siempre, incluida la coerción, pero hoy contamos con ??, ?. y los operadores lógicos de asignación para escribir condiciones más precisas. Las funciones flecha y los parámetros rest hacen innecesario el objeto arguments, y let y const corrigen los problemas de ámbito que causaba var.
En el terreno de la organización del código, los módulos ES sustituyeron al patrón módulo basado en funciones autoejecutables, y las clases con campos privados ofrecen encapsulamiento real sin recurrir a trucos con símbolos o cierres. La programación asíncrona pasó de los callbacks anidados a promesas y async/await, con utilidades como Promise.all y, desde ES2026, Array.fromAsync.
La mejor forma de consolidar estos conceptos es ejecutarlos: abre la consola del navegador o un archivo con Node.js, modifica los ejemplos de Rueda Libre y observa cómo cambia el resultado. Combinado con un linter bien configurado y la costumbre de leer con atención los mensajes de error, ese hábito te permitirá escribir JavaScript moderno, claro y preparado para producción.
