StackPractices
intermediate Por Mathias Paulenko

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 startTransition de 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 await bloquea toda la ejecucion de codigo concurrente
  • Usar setTimeout para 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 — push y splice no 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.