NUCBA
20 de enero de 2026
programación

Tu código asincrónico es un desastre y lo sabés

JavaScript moderno: cómo escribir async/await que no explote en producción y patrones que realmente escalan

Equipo NUCBA

Equipo NUCBA

7 min de lectura

El problema no es la sintaxis

La mayoría de los desarrolladores aprenden async/await en un tutorial con ejemplos perfectos que nunca fallan. Después llegan a un proyecto real y el código se convierte en un callback hell disfrazado de promesas.

El problema no es que no sepas JavaScript. Es que nadie te enseñó a pensar las operaciones asincrónicas como lo que realmente son: cosas que pueden fallar, tardar o nunca resolverse.

Async/await no te salva de vos mismo

Este es el código que escribís cuando aprendés la sintaxis:

async function getUser(id) {
  const user = await fetch(`/api/users/${id}`);
  const data = await user.json();
  return data;
}

Limpio, simple, directo. Ahora probá con un usuario que no existe, un servidor que no responde, o una conexión que se corta a la mitad. Este código explota sin red de contención.

Así es como realmente deberías manejarlo:

async function getUser(id) {
  try {
    const response = await fetch(`/api/users/${id}`, {
      signal: AbortSignal.timeout(5000)
    });
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    
    return await response.json();
  } catch (error) {
    if (error.name === 'TimeoutError') {
      throw new Error('El servidor tardó demasiado en responder');
    }
    throw error;
  }
}

Agregaste timeout, validación de status, y errores específicos. Ahora tenés contexto cuando algo falla.

La regla de oro: nunca confíes en que algo va a funcionar

Cada llamada asincrónica es un punto de falla potencial. Tratá cada await como si fuera una puerta que puede no abrirse.

Patrones que salvan proyectos:

  • Timeout en todo: Si una operación puede tardar para siempre, va a tardar para siempre en producción
  • Retry inteligente: No reintentar infinitamente, sino con backoff exponencial
  • Cancelación: AbortController no es opcional cuando el usuario puede cambiar de pantalla
  • Estados de carga: Loading, error, success. Los tres, siempre

Promises en paralelo (y cuándo no hacerlo)

Este código hace tres requests en serie cuando podría hacerlos en paralelo:

async function getBlogData(userId) {
  const user = await getUser(userId);
  const posts = await getPosts(userId);
  const comments = await getComments(userId);
  return { user, posts, comments };
}

Si cada request tarda 200ms, este código tarda 600ms. Esto es mejor:

async function getBlogData(userId) {
  const [user, posts, comments] = await Promise.all([
    getUser(userId),
    getPosts(userId),
    getComments(userId)
  ]);
  return { user, posts, comments };
}

Ahora tarda 200ms. Pero cuidado: si una falla, todas fallan. A veces querés Promise.allSettled:

async function getBlogData(userId) {
  const results = await Promise.allSettled([
    getUser(userId),
    getPosts(userId),
    getComments(userId)
  ]);
  
  return {
    user: results[0].status === 'fulfilled' ? results[0].value : null,
    posts: results[1].status === 'fulfilled' ? results[1].value : [],
    comments: results[2].status === 'fulfilled' ? results[2].value : []
  };
}

Ahora si los posts fallan, igual podés mostrar el usuario y los comentarios.

Errores que te van a costar horas

1. No esperar en forEach

// Esto NO funciona como esperás
async function processUsers(users) {
  users.forEach(async (user) => {
    await updateUser(user);
  });
  console.log('Terminado'); // Miente. No terminó nada
}

forEach no espera promesas. Usá for...of:

async function processUsers(users) {
  for (const user of users) {
    await updateUser(user);
  }
  console.log('Terminado'); // Ahora sí
}

O Promise.all si querés paralelizar:

async function processUsers(users) {
  await Promise.all(users.map(user => updateUser(user)));
  console.log('Terminado');
}

2. Olvidarte el await

async function saveAndNotify(data) {
  saveToDatabase(data); // Olvidaste await
  notifyUser(); // Esto corre antes de que se guarde
}

Sin await, la función sigue de largo. El dato no se guardó y ya notificaste al usuario.

3. Try/catch que no atrapa nada

function riskyOperation() {
  try {
    setTimeout(async () => {
      await somethingDangerous(); // Este error se pierde
    }, 1000);
  } catch (error) {
    console.log('Nunca llega acá');
  }
}

El try/catch no atrapa errores en callbacks async que se ejecutan después. Movelo adentro:

function riskyOperation() {
  setTimeout(async () => {
    try {
      await somethingDangerous();
    } catch (error) {
      console.log('Ahora sí');
    }
  }, 1000);
}

Patrones reales para código que escala

Race conditions en formularios

El usuario hace clic en "Guardar" tres veces porque el botón no se deshabilitó. Tres requests. Último que llega gana, aunque sea data vieja.

Solución:

let saveInProgress = null;

async function saveForm(data) {
  if (saveInProgress) {
    return saveInProgress; // Devolver la promesa en curso
  }
  
  saveInProgress = (async () => {
    try {
      const result = await api.save(data);
      return result;
    } finally {
      saveInProgress = null;
    }
  })();
  
  return saveInProgress;
}

Debounce de búsquedas

Cada tecla dispara un request. 10 caracteres = 10 requests, 9 innecesarios.

let searchTimeout;

function searchUsers(query) {
  clearTimeout(searchTimeout);
  
  searchTimeout = setTimeout(async () => {
    const results = await api.searchUsers(query);
    displayResults(results);
  }, 300);
}

Mejor aún, con AbortController para cancelar requests en curso:

let currentSearch = null;

async function searchUsers(query) {
  if (currentSearch) {
    currentSearch.abort();
  }
  
  currentSearch = new AbortController();
  
  try {
    const results = await api.searchUsers(query, {
      signal: currentSearch.signal
    });
    displayResults(results);
  } catch (error) {
    if (error.name !== 'AbortError') {
      console.error('Search failed:', error);
    }
  }
}

Performance: lo que realmente importa

Lazy loading

No cargues todo el módulo si solo necesitás una función:

// Malo: carga todo el bundle de moment.js
import moment from 'moment';

button.addEventListener('click', () => {
  const formatted = moment(date).format('DD/MM/YYYY');
});

Mejor:

button.addEventListener('click', async () => {
  const { format } = await import('date-fns');
  const formatted = format(date, 'dd/MM/yyyy');
});

Memoización que sirve

Cachear resultados de funciones caras:

const cache = new Map();

async function getExpensiveData(id) {
  if (cache.has(id)) {
    return cache.get(id);
  }
  
  const data = await fetchExpensiveData(id);
  cache.set(id, data);
  return data;
}

Agregale expiración:

const cache = new Map();

async function getExpensiveData(id, ttl = 60000) {
  const cached = cache.get(id);
  
  if (cached && Date.now() - cached.timestamp < ttl) {
    return cached.data;
  }
  
  const data = await fetchExpensiveData(id);
  cache.set(id, { data, timestamp: Date.now() });
  return data;
}

Lo que realmente necesitás saber

JavaScript moderno no se trata de conocer cada feature nueva. Se trata de escribir código que funciona cuando las cosas fallan, que no bloquea la UI, y que otros pueden mantener sin necesitar un mapa.

Reglas prácticas:

  • Siempre manejá el caso de error, aunque sea con un log
  • Si algo puede cancelarse, tiene que poder cancelarse
  • Paralelizá cuando puedas, pero controlá el caos
  • El código asincrónico no es más difícil, es más honesto sobre lo que puede salir mal

El mejor código JavaScript no es el que usa las features más nuevas. Es el que responde rápido, falla con gracia, y te deja dormir tranquilo después del deploy.

¿Te gustó este artículo?

Descubre nuestros cursos y carreras para llevar tus habilidades al siguiente nivel.