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
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.