Prevencion de Race Conditions en Codigo Async de JavaScript
Identifica y corrige race conditions en JavaScript asincrono usando secuenciacion apropiada, operaciones atomicas y patrones de Promise para ejecucion concurrente predecible
Las race conditions ocurren cuando multiples operaciones async acceden a estado compartido sin coordinacion apropiada, llevando a comportamiento no deterministico. Esta recipe cubre identificar, prevenir y corregir race conditions en JavaScript usando actualizaciones atomicas, secuenciacion apropiada de Promise y patrones de locks.
Cuando Usar Esto
- Multiples llamadas a API actualizan el mismo estado o elementos DOM. Consulta Async Patterns para coordinación.
- Datos cacheados se vuelven stale o inconsistentes bajo acceso concurrente
- Inputs debounced disparan requests de red superpuestas con orden impredecible
Problema
Un componente de busqueda dispara un nuevo request en cada keystroke. Si los resultados llegan fuera de orden, la UI muestra datos stale de una query anterior.
Solucion
1. Cancelacion de Requests con AbortController
// search/SearchService.ts
class SearchService {
private abortController: AbortController | null = null;
async search(query: string): Promise<unknown[]> {
// Cancela request anterior
this.abortController?.abort();
this.abortController = new AbortController();
const response = await fetch(`/api/search?q=${query}`, {
signal: this.abortController.signal,
});
return response.json();
}
}
2. Actualizaciones de Estado Atomicas
// counter/AtomicCounter.ts
class AtomicCounter {
private value = 0;
private queue = Promise.resolve();
increment(): Promise<number> {
this.queue = this.queue.then(async () => {
const current = this.value;
await delay(10);
if (this.value === current) {
this.value = current + 1;
}
return this.value;
});
return this.queue;
}
getValue(): number {
return this.value;
}
}
3. Debounce con Ejecucion Solo-Ultima
// hooks/useLatestQuery.ts
import { useCallback, useRef } from 'react';
function useLatestQuery<T>() {
const latestRequest = useRef(0);
return useCallback(async (query: string, fetcher: (q: string) => Promise<T>): Promise<T> => {
const requestId = ++latestRequest.current;
const result = await fetcher(query);
if (requestId !== latestRequest.current) {
throw new Error('Stale request');
}
return result;
}, []);
}
4. Mutex Lock para Secciones Criticas
// locks/Mutex.ts
class Mutex {
private locked = false;
private queue: Array<() => void> = [];
async acquire(): Promise<() => void> {
if (!this.locked) {
this.locked = true;
return () => this.release();
}
return new Promise((resolve) => {
this.queue.push(() => resolve(() => this.release()));
});
}
private release(): void {
if (this.queue.length > 0) {
const next = this.queue.shift()!;
next();
} else {
this.locked = false;
}
}
}
const balanceMutex = new Mutex();
async function transfer(from: Account, to: Account, amount: number): Promise<void> {
const release = await balanceMutex.acquire();
try {
if (from.balance >= amount) {
from.balance -= amount;
to.balance += amount;
}
} finally {
release();
}
}
5. Patron Compare-and-Swap
// storage/CASStore.ts
class CASStore<T> {
private value: T;
constructor(initial: T) {
this.value = initial;
}
compareAndSwap(expected: T, newValue: T): boolean {
if (this.value === expected) {
this.value = newValue;
return true;
}
return false;
}
getValue(): T {
return this.value;
}
}
6. Ejemplo de Lock con Python Asyncio
import asyncio
class Account:
def __init__(self, balance: float):
self.balance = balance
async def transfer(from_acc: Account, to_acc: Account, amount: float, lock: asyncio.Lock):
async with lock:
if from_acc.balance >= amount:
from_acc.balance -= amount
to_acc.balance += amount
return True
return False
async def main():
account_a = Account(1000)
account_b = Account(500)
lock = asyncio.Lock()
# Transferencias concurrentes son serializadas por el lock
results = await asyncio.gather(
transfer(account_a, account_b, 200, lock),
transfer(account_a, account_b, 300, lock),
transfer(account_b, account_a, 100, lock),
)
print(f"A: {account_a.balance}, B: {account_b.balance}")
print(f"Results: {results}")
asyncio.run(main())
Sin el lock, transferencias concurrentes podrian leer from_acc.balance antes de cualquier deduccion, causando saldos negativos. El asyncio.Lock asegura que solo una transferencia se ejecute a la vez.
Como Funciona
- AbortController cancela requests en vuelo cuando son superados
- Colas atomicas serializan operaciones sobre estado compartido
- Request IDs ignoran responses de llamadas desactualizadas
- Mutex locks enforcean exclusion mutua en secciones criticas
- Operaciones CAS reintentan actualizaciones cuando se detectan modificaciones concurrentes
Consideraciones de Produccion
- Usa
startTransitionde React para actualizaciones de estado no-urgentes para evitar bloqueo de UI - Implementa optimistic updates con rollback en fallo para mejor rendimiento percibido
- Monitorea sintomas de race conditions con Sentry o similar error tracking
Errores Comunes
- Leer estado antes de una operacion async y usar el valor stale despues
- No limpiar event listeners o timers que modifican estado compartido
- Asumir que
awaitbloquea toda la ejecucion de codigo concurrente - Usar
setTimeoutpara ordenar en lugar de secuenciacion async apropiada - Olvidar cancelar requests pendientes cuando un componente se desmonta en React
- Modificar arrays u objetos compartidos sin sincronizacion —
pushyspliceno son atomicos a traves de boundaries de await - Mantener locks a traves de llamadas de red, lo que crea tiempos de espera largos y potenciales deadlocks
Troubleshooting
- Race conditions appear under load: protect shared state with locks, atomics, or message passing. Reproduce with targeted stress tests.
- Deadlock between workers: establish a consistent lock acquisition order and keep critical sections short.
- Thread pool saturation: Increase pool size only if CPU and memory allow.
- Actor mailbox grows unbounded: apply backpressure, bounded queues, and load shedding.
- Async task never completes: check for unhandled promise rejections, forgotten awaits, and infinite loops in cooperative scheduling.
Referencia Rápida
- Comando principal: ejecuta la solución base del artículo y verifica el resultado esperado.
- Validación: confirma que los tests pasan y que las métricas clave no se degradaron.
- Rollback: si algo falla, revierte el cambio y consulta la sección de Troubleshooting.
Lectura Adicional
- Documentación oficial: consulta la referencia actualizada del framework o herramienta utilizada.
- Guías relacionadas: explora las guías de race-condition y concurrency para profundizar.
- Patrones complementarios: revisa los patrones de diseño aplicables a tu stack tecnológico.
- Postmortems públicos: estudia incidentes reales de equipos que enfrentaron problemas similares en producción.
Notas de Producción
- Despliega gradualmente usando canary o blue-green para detectar regresiones temprano.
- Configura alertas para errores, latencia p99 y tasa de fallos antes de habilitar en producción.
- Documenta el rollback en el runbook; prueba el procedimiento en staging al menos una vez por trimestre.
- Revisa logs estructurados con correlation IDs para trazar requests end-to-end en incidentes.
Puntos Clave
- Aplica prevencion de race conditions en codigo async de javascript cuando necesites una solución práctica para tu caso de uso.
- Monitorea el rendimiento después de implementar; mide latencia, errores y uso de recursos antes y después.
- Revisa la sección de Troubleshooting ante errores comunes; la mayoría tienen causa raíz documentada con solución.
- Mantén dependencias actualizadas y ejecuta tests en CI para prevenir regresiones en producción.
Errores Comunes en Producción
- Copiar el ejemplo sin adaptarlo a volúmenes y modos de fallo reales.
- Saltar tests de carga e inyección de errores antes del primer despliegue productivo.
- Codificar valores fijos que deberían ser configurables por entorno.
- Olvidar agregar logging y monitoreo en cada paso.
- Desplegar sin plan de rollback ni estrategia de backup probada.
- Asumir que el ejemplo mínimo escalará sin agregar caché o procesamiento por lotes.
- No documentar la versión y configuración usadas en producción.
- Dejar la receta sin cambios cuando evolucionan las dependencias o la escala.
Preguntas frecuentes
En que se diferencia de un deadlock?
Las race conditions producen resultados incorrectos por acceso concurrente. Los deadlocks ocurren cuando threads se bloquean indefinidamente esperando recursos.
Necesito locks en JavaScript single-threaded?
JavaScript es single-threaded pero las operaciones async se intercalan. El estado aun puede corromperse entre puntos de await.
Como pruebo race conditions?
Escribe tests que ejecuten operaciones concurrentes y verifiquen el estado final. Usa Promise.all para disparar llamadas paralelas. Inyecta delays aleatorios con await delay(Math.random() * 100) para aumentar la probabilidad de detectar bugs de intercalacion. Ejecuta tests multiples veces — las race conditions son no deterministas.
Cual es la diferencia entre concurrencia optimista y locking pesimista?
La concurrencia optimista asume que no hay conflicto y reintenta en fallo (patron CAS). El locking pesimista adquiere un lock antes de acceder estado compartido (Mutex). Usa concurrencia optimista cuando los conflictos son raros. Usa locking pesimista cuando los conflictos son frecuentes o reintentar es costoso.
Puedo usar Promise.all con seguridad con estado compartido?
Solo si cada promise opera con datos independientes. Si los promises leen y escriben la misma variable, Promise.all no los serializa — se intercalan en puntos de await. Usa un mutex o serializa las operaciones con una cadena de promises.
Recursos Relacionados
Paralelizar Trabajo CPU y I/O con ThreadPoolExecutor
Usar ThreadPoolExecutor de Python para operaciones I/O paralelas, recoleccion thread-safe de resultados, callbacks de Future, manejo de errores y mezcla de threads con asyncio para trabajo bloqueante.
RecipeEjecutar Tareas Async Concurrentes con asyncio.gather y
Ejecutar multiples operaciones async concurrentemente en Python usando asyncio.gather, asyncio.TaskGroup, manejo de errores con return_exceptions, timeouts y semaforos para rate limiting.
PatternPatrón de Consumidor Idempotente
Procesa mensajes de una cola exactamente una vez sin importar los duplicados, usando operaciones idempotentes, identificadores únicos y estrategias de deduplicación.
RecipeEvent Loop de JavaScript
Comprende cómo funciona el event loop de JavaScript y cómo escribir código no bloqueante.
RecipeColecciones Thread-Safe: Blocking Queues y Concurrent Maps
Cómo compartir colecciones entre hilos de forma segura usando estructuras concurrentes: colas, mapas, listas y contadores atómicos en Java, Python y C++.