Asincronía

JavaScript es single-threaded: ejecuta un solo hilo de código a la vez. Pero la web necesita hacer cosas que toman tiempo (pedir datos a un servidor, leer un archivo, esperar un timer) sin congelar la interfaz. La asincronía es el mecanismo que permite al navegador "posponer" ciertas operaciones y continuar ejecutando el resto del código mientras espera. Dominar callbacks, Promises, async/await y la Fetch API es esencial para cualquier aplicación web moderna que se comunique con servidores o maneje datos en tiempo real.

Visualizá cómo funciona el Event Loop con la animación del call stack y las colas, experimentá con los estados de las Promises y sus métodos estáticos (all, allSettled, race, any), compará async/await secuencial vs paralelo con timelines, simulá peticiones Fetch con distintos métodos y errores, y explorá 4 patrones de manejo de errores.

Async Playground

Callbacks

Un callback es simplemente una función que se pasa como argumento a otra función, con la intención de que se ejecute más adelante (después de que alguna operación asíncrona termine). Es el mecanismo más básico de asincronía en JavaScript. Funciones como setTimeout, addEventListener y los métodos de arrays como forEach y map todos usan callbacks internamente. El navegador o Node.js se encargan de llamar a tu callback cuando corresponda.

JavaScript
// === Callback basico ===
// La funcion recibe otra funcion como parametro
function saludar(nombre, callback) {
    console.log(`Hola, ${nombre}`);
    callback(); // se ejecuta despues
}

saludar("Carlos", () => {
    console.log("Esto se ejecuta al final");
});

// === Callback asincrono con setTimeout ===
console.log("1. Inicio");

setTimeout(() => {
    console.log("2. Callback ejecutado despues de 1 segundo");
}, 1000);

console.log("3. Fin");
// Orden de salida: 1, 3, 2 (el callback se pospone)

// === Simular una operacion asincrona con callback ===
function obtenerUsuario(id, callback) {
    setTimeout(() => {
        const usuario = { id, nombre: "Ana", email: "ana@mail.com" };
        callback(null, usuario); // convencion: primer arg = error
    }, 500);
}

obtenerUsuario(1, (error, usuario) => {
    if (error) {
        console.error("Error:", error);
        return;
    }
    console.log("Usuario:", usuario.nombre);
});

// === Event listeners tambien son callbacks ===
document.querySelector("button").addEventListener("click", (e) => {
    console.log("Boton clickeado");
    // esta funcion se ejecuta "cuando" el evento ocurra
});

El problema con los callbacks es lo que se conoce como callback hell (o "pirámide de la perdición"): cuando necesitás encadenar múltiples operaciones asíncronas una después de otra, el código se va indentando cada vez más hacia la derecha, volviéndose difícil de leer, mantener y depurar. Cada nuevo nivel de anidación hace que el código sea más frágil. Aunque se puede mitigar nombrando funciones en vez de usar anónimas, la solución real son las Promises.

JavaScript
// === CALLBACK HELL (evitar esto!) ===

obtenerUsuario(1, (error, usuario) => {
    if (error) return console.error(error);

    obtenerPosts(usuario.id, (error, posts) => {
        if (error) return console.error(error);

        obtenerComentarios(posts[0].id, (error, comentarios) => {
            if (error) return console.error(error);

            obtenerAutor(comentarios[0].autorId, (error, autor) => {
                if (error) return console.error(error);

                console.log("Autor del primer comentario:", autor);
                // cada nivel mas indentado... inmanejable
            });
        });
    });
});

// === Alternativa: funciones nombradas (mejor pero no ideal) ===
function manejarAutor(error, autor) {
    if (error) return console.error(error);
    console.log("Autor:", autor);
}

function manejarComentarios(error, comentarios) {
    if (error) return console.error(error);
    obtenerAutor(comentarios[0].autorId, manejarAutor);
}

function manejarPosts(error, posts) {
    if (error) return console.error(error);
    obtenerComentarios(posts[0].id, manejarComentarios);
}

obtenerUsuario(1, (error, usuario) => {
    if (error) return console.error(error);
    obtenerPosts(usuario.id, manejarPosts);
});

Callbacks: úsalos solo cuando sea simple

Los callbacks siguen siendo válidos para operaciones simples y únicas (un setTimeout, un addEventListener, un forEach). Pero para encadenar múltiples operaciones asíncronas, usá Promises o async/await. No escribas callback hell en código nuevo; es considerado un antipatrón desde ES6.

Promises

Una Promise es un objeto que representa el resultado eventual de una operación asíncrona. Puede estar en tres estados: pending (pendiente, aún no se resolvió), fulfilled (resuelta con éxito) o rejected (rechazada con un error). Una vez que una promise se resuelve o rechaza, no cambia de estado. Las promesas resuelven el problema del callback hell al permitir encadenar operaciones con .then() en vez de anidar callbacks, lo que produce código más plano y legible.

JavaScript
// === Crear una Promise ===
const miPromise = new Promise((resolve, reject) => {
    const exito = true;

    if (exito) {
        resolve("Operacion exitosa!"); // resuelve la promise
    } else {
        reject("Algo salio mal");      // rechaza la promise
    }
});

// === Consumir una Promise con .then() / .catch() ===
miPromise
    .then((resultado) => {
        console.log(resultado); // "Operacion exitosa!"
        return "dato procesado";
    })
    .then((dato) => {
        console.log("Segundo then:", dato);
    })
    .catch((error) => {
        console.error("Error:", error);
    })
    .finally(() => {
        console.log("Siempre se ejecuta, haya o no error");
    });

// === Ejemplo real: simular una peticion ===
function fetchUsuario(id) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (id > 0) {
                resolve({ id, nombre: "Ana", rol: "admin" });
            } else {
                reject(new Error("ID de usuario invalido"));
            }
        }, 300);
    });
}

fetchUsuario(1)
    .then(usuario => console.log(usuario))
    .catch(error => console.error(error.message));

El método .then() recibe la función que se ejecuta cuando la promise se resuelve, y .catch() captura cualquier error que ocurra en la cadena (ya sea un reject explícito o una excepción lanzada dentro de un .then()). .finally() se ejecuta siempre al final, sin importar si fue éxito o error, y es ideal para limpieza (ocultar un spinner, cerrar una conexión, etc.). El valor que devuelve un .then() se pasa automáticamente al siguiente .then() de la cadena, permitiendo un flujo de datos limpio y secuencial.

JavaScript
// === Encadenar promesas (resolviendo el callback hell) ===

function obtenerPosts(userId) {
    return new Promise((resolve) => {
        setTimeout(() => resolve([
            { id: 1, titulo: "Mi primer post", userId },
            { id: 2, titulo: "Aprendiendo JS", userId }
        ]), 300);
    });
}

function obtenerComentarios(postId) {
    return new Promise((resolve) => {
        setTimeout(() => resolve([
            { id: 1, postId, texto: "Excelente post!" },
            { id: 2, postId, texto: "Muy util" }
        ]), 300);
    });
}

// Con promesas: plano y legible
fetchUsuario(1)
    .then(usuario => {
        console.log("Usuario:", usuario.nombre);
        return obtenerPosts(usuario.id); // devuelve otra promise
    })
    .then(posts => {
        console.log("Posts:", posts.length);
        return obtenerComentarios(posts[0].id);
    })
    .then(comentarios => {
        console.log("Comentarios:", comentarios);
    })
    .catch(error => {
        console.error("Error en la cadena:", error.message);
    });

// === Metodos estaticos de Promise ===

// Promise.all — esperar a que TODAS se resuelvan
const p1 = fetchUsuario(1);
const p2 = fetchUsuario(2);
const p3 = fetchUsuario(3);

Promise.all([p1, p2, p3])
    .then(usuarios => {
        console.log("Todos los usuarios:", usuarios);
        // usuarios es un array con los resultados de cada promise
    })
    .catch(error => {
        // Si CUALQUIERA falla, todo falla
        console.error("Al menos una fallo:", error);
    });

// Promise.allSettled — espera a todas, no falla si alguna falla
Promise.allSettled([p1, p2, fetchUsuario(-1)])
    .then(resultados => {
        resultados.forEach(r => {
            if (r.status === "fulfilled") {
                console.log("Exito:", r.value.nombre);
            } else {
                console.log("Fallo:", r.reason.message);
            }
        });
    });

// Promise.race — la primera en resolver (o rechazar) gana
Promise.race([
    fetch("https://api.ejemplo.com/fast"),
    fetch("https://api.ejemplo.com/slow")
])
    .then(response => console.log("Primera respuesta:", response.url));

// Promise.any — la primera en RESOLVER (ignora rechazos)
Promise.any([
    Promise.reject("error 1"),
    Promise.resolve("exito 2"),
    Promise.reject("error 3")
])
    .then(valor => console.log(valor)); // "exito 2"

// Promise.resolve / Promise.reject — crear promesas ya resueltas
const yaResuelta = Promise.resolve(42);
yaResuelta.then(v => console.log(v)); // 42

const yaRechazada = Promise.reject(new Error("falló"));
yaRechazada.catch(e => console.error(e.message));

Promise.all vs Promise.allSettled vs Promise.any

Usá Promise.all() cuando necesitás que todas las operaciones tengan éxito (si una falla, todo falla). Usá Promise.allSettled() cuando querés los resultados de todas, sea cual sea su resultado (útil para dashboards donde algunos datos pueden fallar). Usá Promise.any() cuando te sirve el primer resultado exitoso (como fallback entre múltiples CDNs). Y Promise.race() cuando te importa cuál termina primero, sea éxito o error (como un timeout).

Async / Await

async/await es azúcar sintáctico sobre Promises que hace el código asíncrono lucir y comportarse como código síncrono. La palabra clave async antes de una función la convierte automáticamente en una función que devuelve una Promise. La palabra clave await pausa la ejecución de esa función hasta que la Promise se resuelva, y luego devuelve el valor resuelto. Esto elimina la necesidad de encadenar .then() y hace que el flujo del código sea mucho más fácil de seguir, especialmente cuando hay múltiples operaciones secuenciales.

JavaScript
// === Comparacion: Promises vs async/await ===

// CON PROMISES (.then encadenado)
function cargarDatos() {
    return fetchUsuario(1)
        .then(usuario => {
            return obtenerPosts(usuario.id);
        })
        .then(posts => {
            return obtenerComentarios(posts[0].id);
        })
        .then(comentarios => {
            console.log(comentarios);
        })
        .catch(error => {
            console.error(error);
        });
}

// CON ASYNC/AWAIT (mucho mas legible)
async function cargarDatosAsync() {
    try {
        const usuario = await fetchUsuario(1);
        const posts = await obtenerPosts(usuario.id);
        const comentarios = await obtenerComentarios(posts[0].id);
        console.log(comentarios);
    } catch (error) {
        console.error(error.message);
    }
}

// === Reglas de async/await ===

// 1. await solo funciona DENTRO de una funcion async
async function ejemplo() {
    const data = await fetchUsuario(1); // OK
    console.log(data);
}

// 2. Toda funcion async devuelve una Promise
async function saludar() {
    return "Hola"; // esto se envuelve en Promise.resolve("Hola")
}
saludar().then(msg => console.log(msg)); // "Hola"

// 3. Si la funcion async lanza un error, la Promise se rechaza
async function fallar() {
    throw new Error("algo rompio");
}
fallar().catch(e => console.error(e.message)); // "algo rompio"

// 4. await con valores que no son Promise funciona igual
async function ejemplo2() {
    const num = await 42;           // se resuelve inmediatamente
    const texto = await "hola";     // idem
    console.log(num, texto);        // 42 "hola"
}

// === Manejo de errores con try/catch ===
async function obtenerDatosSeguro() {
    try {
        const usuario = await fetchUsuario(1);
        const posts = await obtenerPosts(usuario.id);
        return { usuario, posts };
    } catch (error) {
        console.error("Fallo al obtener datos:", error.message);
        return null; // devolver un valor por defecto
    } finally {
        // Siempre se ejecuta (ocultar spinner, etc.)
        console.log("Operacion finalizada");
    }
}

// === Ejecutar multiples promesas en paralelo ===
async function cargarTodo() {
    // Esto es SECUENCIAL (lento - cada uno espera al anterior)
    const u1 = await fetchUsuario(1);
    const u2 = await fetchUsuario(2);
    const u3 = await fetchUsuario(3);

    // Esto es PARALELO (rapido - todas arrancan al mismo tiempo)
    const [u1, u2, u3] = await Promise.all([
        fetchUsuario(1),
        fetchUsuario(2),
        fetchUsuario(3)
    ]);
    console.log(u1, u2, u3);
}

// === Top-level await (en modulos ES) ===
// En archivos .mjs o con type="module", podes usar await fuera de funciones
// const datos = await fetchUsuario(1);

Un error común es usar await en un loop para operaciones que podrían ser paralelas. Si tenés que hacer múltiples peticiones independientes (por ejemplo, obtener 10 usuarios distintos), no hagas un for con await adentro porque cada petición esperará a la anterior. En su lugar, generá un array de promesas y usá Promise.all() para ejecutarlas todas al mismo tiempo. Solo usá await secuencial cuando una operación depende del resultado de la anterior.

JavaScript
// === MAL: await secuencial en loop (lento) ===
async function procesarIdsSecuencial(ids) {
    const resultados = [];
    for (const id of ids) {
        const usuario = await fetchUsuario(id); // espera cada uno
        resultados.push(usuario);
    }
    return resultados;
}

// === BIEN: paralelo con Promise.all (rapido) ===
async function procesarIdsParalelo(ids) {
    const promesas = ids.map(id => fetchUsuario(id));
    const resultados = await Promise.all(promesas);
    return resultados;
}

// === Pero si CADA operacion depende de la anterior, await secuencial OK ===
async function procesarCadena(userId) {
    const usuario = await fetchUsuario(userId);         // necesita userId
    const posts = await obtenerPosts(usuario.id);       // necesita usuario.id
    const comentarios = await obtenerComentarios(posts[0].id); // necesita posts[0]
    return comentarios;
}

// === Ejecutar en lotes (chunked parallelism) ===
async function procesarEnLotes(ids, tamanoLote = 5) {
    const resultados = [];

    for (let i = 0; i < ids.length; i += tamanoLote) {
        const lote = ids.slice(i, i + tamanoLote);
        const loteResultados = await Promise.all(
            lote.map(id => fetchUsuario(id))
        );
        resultados.push(...loteResultados);
    }

    return resultados;
}
// Procesa de a 5 en 5 para no saturar el servidor

Siempre usá try/catch con async/await

A diferencia de las Promises con .catch(), los errores en async/await no se capturan automáticamente. Si un await falla y no hay un try/catch, la excepción se propaga como una Promise rechazada. Si nadie la captura, verás un UnhandledPromiseRejection en la consola. Siempre envuelve tus await en try/catch, o maneja el error en el llamador con .catch().

Fetch API

La Fetch API es la interfaz moderna de JavaScript para hacer peticiones HTTP. Reemplazó al viejo XMLHttpRequest con una API mucho más limpia basada en Promises. fetch() devuelve una Promise que se resuelve con un objeto Response. Es importante saber que fetch no lanza error cuando el servidor responde con un status 4xx o 5xx: la Promise se resuelve correctamente y tenés que verificar response.ok o response.status manualmente para detectar errores HTTP.

JavaScript
// === GET request basico ===
async function getUsuarios() {
    try {
        const response = await fetch("https://jsonplaceholder.typicode.com/users");

        // IMPORTANTE: fetch no lanza error en 404 o 500
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }

        const usuarios = await response.json(); // parsea el body como JSON
        console.log(usuarios);
        return usuarios;
    } catch (error) {
        console.error("Error fetching usuarios:", error.message);
        return [];
    }
}

// === Otros metodos de lectura del body ===
// response.text()   — devuelve el body como string
// response.json()   — parsea como JSON (devuelve Promise)
// response.blob()   — devuelve un Blob (para archivos)
// response.formData() — devuelve FormData
// response.arrayBuffer() — devuelve ArrayBuffer (datos binarios)

// === GET con query parameters ===
async function buscarUsuarios(query) {
    const params = new URLSearchParams({
        q: query,
        limit: "10",
        sort: "nombre"
    });

    const response = await fetch(
        `https://api.ejemplo.com/users?${params.toString()}`
    );
    return response.json();
}

// === POST — enviar datos al servidor ===
async function crearUsuario(datos) {
    try {
        const response = await fetch("https://jsonplaceholder.typicode.com/users", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "Authorization": "Bearer mi-token"
            },
            body: JSON.stringify(datos) // convertir objeto a string JSON
        });

        if (!response.ok) {
            throw new Error(`Error al crear: ${response.status}`);
        }

        const nuevoUsuario = await response.json();
        console.log("Creado:", nuevoUsuario);
        return nuevoUsuario;
    } catch (error) {
        console.error(error.message);
    }
}

crearUsuario({ nombre: "Ana", email: "ana@mail.com" });

// === PUT — actualizar un recurso completo ===
async function actualizarUsuario(id, datos) {
    const response = await fetch(`https://api.ejemplo.com/users/${id}`, {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(datos)
    });
    return response.json();
}

// === PATCH — actualizar parcialmente un recurso ===
async function parchearUsuario(id, datosParciales) {
    const response = await fetch(`https://api.ejemplo.com/users/${id}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(datosParciales)
    });
    return response.json();
}

// === DELETE — eliminar un recurso ===
async function eliminarUsuario(id) {
    const response = await fetch(`https://api.ejemplo.com/users/${id}`, {
        method: "DELETE"
    });

    if (!response.ok) {
        throw new Error(`Error al eliminar: ${response.status}`);
    }

    // DELETE puede devolver 204 No Content (sin body)
    console.log("Usuario eliminado");
}

fetch NO lanza error en respuestas 4xx/5xx

Este es uno de los errores más comunes. fetch solo rechaza la Promise si hay un error de red (no hay conexión, DNS falló, CORS bloqueó). Un status 404 (Not Found) o 500 (Server Error) se considera una respuesta "exitosa" desde la perspectiva de la red. Siempre verificá response.ok (que es true para status 200-299) o revisá response.status antes de procesar los datos.

Headers y configuración avanzada

Los headers HTTP son metadatos que se envían junto con la petición y la respuesta. Permiten especificar el formato de los datos, autenticación, caché, y muchas otras cosas. En Fetch, podés manejar headers de forma individual con el objeto Headers o pasarlos directamente como un objeto plano. Además, fetch acepta opciones avanzadas como mode (para CORS), cache, redirect, credentials y signal (para cancelar peticiones).

JavaScript
// === Headers: objeto plano (mas comun) ===
const response = await fetch(url, {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "Authorization": "Bearer abc123",
        "Accept": "application/json",
        "X-Custom-Header": "valor"
    },
    body: JSON.stringify({ nombre: "Ana" })
});

// === Headers: objeto Headers (mas metodos) ===
const headers = new Headers();
headers.append("Content-Type", "application/json");
headers.set("Authorization", "Bearer abc123");
headers.has("Content-Type");  // true
headers.get("Content-Type");  // "application/json"
headers.delete("X-Old-Header");

// === Leer headers de la respuesta ===
const contentType = response.headers.get("Content-Type");
const contentLength = response.headers.get("Content-Length");

// === Opciones avanzadas de fetch ===

// mode: "cors" (default) | "no-cors" | "same-origin"
// Usar "no-cors" para peticiones opacas (no podes leer la respuesta)
await fetch(url, { mode: "cors" });

// credentials: "same-origin" (default) | "include" | "omit"
// "include" envia cookies en peticiones cross-origin
await fetch(url, { credentials: "include" });

// cache: "default" | "no-store" | "reload" | "force-cache"
await fetch(url, { cache: "no-store" });

// redirect: "follow" (default) | "error" | "manual"
await fetch(url, { redirect: "manual" });

// === Cancelar una peticion con AbortController ===
const controller = new AbortController();
const signal = controller.signal;

// Cancelar despues de 5 segundos
setTimeout(() => controller.abort(), 5000);

try {
    const response = await fetch(url, { signal });
    const data = await response.json();
} catch (error) {
    if (error.name === "AbortError") {
        console.log("Peticion cancelada por timeout");
    } else {
        console.error("Otro error:", error);
    }
}

// === AbortController con un timeout wrapper reutilizable ===
function fetchWithTimeout(url, options = {}, timeout = 5000) {
    const controller = new AbortController();
    const id = setTimeout(() => controller.abort(), timeout);

    return fetch(url, { ...options, signal: controller.signal })
        .finally(() => clearTimeout(id));
}

// Uso
const data = await fetchWithTimeout(
    "https://api.lenta.com/datos",
    {},
    3000 // 3 segundos de timeout
);

AbortController para cancelar peticiones

Siempre que hagas una petición que pueda ser cancelada (por ejemplo cuando el usuario navega a otra página antes de que llegue la respuesta, o querés implementar un timeout), usá AbortController. Es la forma estándar de cancelar fetch requests. También es útil para implementar búsquedas "debounced" donde cada nueva letra cancela la petición anterior.

API REST y Status Codes

REST (Representational State Transfer) es el estilo de arquitectura más común para APIs web. Se basa en usar los métodos HTTP (GET, POST, PUT, PATCH, DELETE) para operar sobre recursos identificados por URLs. Cada URL representa un "recurso" (un usuario, un post, un producto), y el método HTTP indica qué acción querés realizar sobre ese recurso. Entender los códigos de estado HTTP es fundamental para manejar correctamente las respuestas del servidor y dar feedback adecuado al usuario.

JavaScript
// === Clase wrapper para una API REST ===
class ApiClient {
    constructor(baseUrl) {
        this.baseUrl = baseUrl;
        this.defaultHeaders = {
            "Content-Type": "application/json"
        };
    }

    // Helper para manejar la respuesta
    async handleResponse(response) {
        if (!response.ok) {
            const errorData = await response.json().catch(() => ({}));
            const error = new Error(errorData.message || response.statusText);
            error.status = response.status;
            throw error;
        }

        // 204 No Content no tiene body
        if (response.status === 204) return null;

        return response.json();
    }

    setAuthToken(token) {
        this.defaultHeaders["Authorization"] = `Bearer ${token}`;
    }

    // GET /users
    async get(endpoint) {
        const response = await fetch(`${this.baseUrl}${endpoint}`, {
            headers: this.defaultHeaders
        });
        return this.handleResponse(response);
    }

    // POST /users
    async post(endpoint, data) {
        const response = await fetch(`${this.baseUrl}${endpoint}`, {
            method: "POST",
            headers: this.defaultHeaders,
            body: JSON.stringify(data)
        });
        return this.handleResponse(response);
    }

    // PUT /users/1
    async put(endpoint, data) {
        const response = await fetch(`${this.baseUrl}${endpoint}`, {
            method: "PUT",
            headers: this.defaultHeaders,
            body: JSON.stringify(data)
        });
        return this.handleResponse(response);
    }

    // DELETE /users/1
    async delete(endpoint) {
        const response = await fetch(`${this.baseUrl}${endpoint}`, {
            method: "DELETE",
            headers: this.defaultHeaders
        });
        return this.handleResponse(response);
    }
}

// === Uso del wrapper ===
const api = new ApiClient("https://jsonplaceholder.typicode.com");

// Obtener todos los posts
const posts = await api.get("/posts");

// Crear un nuevo post
const nuevoPost = await api.post("/posts", {
    title: "Mi post",
    body: "Contenido del post",
    userId: 1
});

// Actualizar un post
await api.put("/posts/1", { title: "Titulo actualizado" });

// Eliminar un post
await api.delete("/posts/1");

Los códigos de estado HTTP se agrupan por familias según su primer dígito. Los 2xx son éxitos (200 OK, 201 Created, 204 No Content). Los 3xx son redirecciones (301 Moved Permanently, 304 Not Modified). Los 4xx son errores del cliente (400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 422 Unprocessable Entity, 429 Too Many Requests). Los 5xx son errores del servidor (500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable). En tu código, cada familia debería tener un manejo distinto: los 4xx generalmente muestran un mensaje al usuario, los 5xx pueden indicar que deberías reintentar la operación.

Status codes más comunes

200 OK — Todo bien, acá están los datos. 201 Created — Recurso creado con éxito (respuesta a POST). 204 No Content — Éxito pero sin body (respuesta a DELETE). 400 Bad Request — Los datos enviados son inválidos. 401 Unauthorized — No enviaste token o es inválido. 403 Forbidden — No tenés permiso (el token es válido pero no podés). 404 Not Found — El recurso no existe. 429 Too Many Requests — Rate limit, esperá un poco. 500 Internal Server Error — Algo rompió en el servidor.

Manejo de errores y patrones UI

Una aplicación robusta necesita manejar errores de forma elegante. Cuando haces peticiones a un servidor, pueden fallar por muchas razones: no hay conexión, el servidor está caído, el usuario no tiene permisos, los datos son inválidos, etc. El patrón básico es mostrar un loading state mientras se espera la respuesta, un error state si algo falla, y el contenido cuando todo sale bien. Este ciclo de tres estados (loading, error, success) se repite constantemente en cualquier aplicación web que consuma datos de una API.

JavaScript
// === Patron: Loading / Error / Success ===

const container = document.querySelector("#user-container");

async function cargarYMostrarUsuario(id) {
    // 1. LOADING STATE
    container.innerHTML = `
        <div class="loading">
            <div class="spinner"></div>
            <p>Cargando usuario...</p>
        </div>
    `;

    try {
        // 2. PETICION
        const response = await fetch(`https://api.ejemplo.com/users/${id}`);

        if (!response.ok) {
            throw new Error(`Error ${response.status}`);
        }

        const usuario = await response.json();

        // 3. SUCCESS STATE
        container.innerHTML = `
            <div class="user-card">
                <h3>${usuario.nombre}</h3>
                <p>${usuario.email}</p>
            </div>
        `;
    } catch (error) {
        // 4. ERROR STATE
        let mensaje = "Ocurrio un error inesperado";

        if (error.name === "AbortError") {
            mensaje = "La peticion tardó demasiado. Intenta de nuevo.";
        } else if (!navigator.onLine) {
            mensaje = "No hay conexion a internet. Verifica tu red.";
        } else if (error.message.includes("404")) {
            mensaje = "El usuario no fue encontrado.";
        } else if (error.message.includes("401")) {
            mensaje = "Tu sesion expiró. Inicia sesion de nuevo.";
        }

        container.innerHTML = `
            <div class="error-state">
                <p class="error-icon">!</p>
                <p>${mensaje}</p>
                <button onclick="cargarYMostrarUsuario(${id})">
                    Reintentar
                </button>
            </div>
        `;
    }
}

// === Patron: Retry con backoff exponencial ===
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
    for (let intento = 1; intento <= maxRetries; intento++) {
        try {
            const response = await fetch(url, options);

            if (!response.ok && response.status >= 500 && intento < maxRetries) {
                // Error del servidor: reintentar
                const delay = Math.pow(2, intento) * 1000; // 2s, 4s, 8s
                console.log(`Intento ${intento} falló, reintentando en ${delay}ms`);
                await new Promise(resolve => setTimeout(resolve, delay));
                continue;
            }

            if (!response.ok) {
                throw new Error(`HTTP ${response.status}`);
            }

            return await response.json();
        } catch (error) {
            if (intento === maxRetries) throw error;

            const delay = Math.pow(2, intento) * 1000;
            await new Promise(resolve => setTimeout(resolve, delay));
        }
    }
}

// === Patron: guardar en cache (simple) ===
const cache = new Map();

async function fetchConCache(url) {
    if (cache.has(url)) {
        console.log("Datos desde cache");
        return cache.get(url);
    }

    const response = await fetch(url);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);

    const data = await response.json();
    cache.set(url, data);
    return data;
}

// === Patron: debounce para busquedas ===
function debounce(fn, delay) {
    let timer;
    return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => fn(...args), delay);
    };
}

// Uso con fetch: cada tecla cancela la busqueda anterior
const buscador = document.querySelector("#buscador");
const controller = new AbortController();

buscador.addEventListener("input", debounce(async (e) => {
    // Cancelar peticion anterior
    controller.abort();

    const query = e.target.value.trim();
    if (query.length < 3) return;

    try {
        const response = await fetch(
            `https://api.ejemplo.com/search?q=${encodeURIComponent(query)}`,
            { signal: controller.signal }
        );

        if (!response.ok) throw new Error(`HTTP ${response.status}`);

        const resultados = await response.json();
        mostrarResultados(resultados);
    } catch (error) {
        if (error.name !== "AbortError") {
            console.error("Error en busqueda:", error);
        }
    }
}, 300));

El backoff exponencial es un patrón donde cada reintentó espera más tiempo que el anterior (2s, 4s, 8s...). Esto evita saturar un servidor que ya está teniendo problemas. El debounce retrasa la ejecución de una función hasta que deje de ser llamada por un período (por ejemplo, esperar 300ms después de la última tecla antes de buscar). Combinando debounce con AbortController, cada nueva tecla cancela la petición anterior y solo se procesa la última, lo que es ideal para autocompletados y búsquedas en tiempo real.

La regla de los 3 estados

Toda operación asíncrona que afecte la UI debería tener tres estados: loading (mientras espera), error (si falla) y data (cuando tiene éxito). Nunca dejes la interfaz sin feedback mientras se carga algo. Un spinner, un skeleton, o un texto "Cargando..." es siempre mejor que una pantalla en blanco que parece congelada.