Una pantalla muestra datos antiguos cuando dos solicitudes terminan en un orden distinto al de su inicio. La prueba suele pasar porque la red fue rápida ese día. Después alguien la ejecuta una y otra vez, esperando que aparezca la carrera.
Esa prueba no elige la secuencia que necesitas investigar. Para probar una condición de carrera en JavaScript, mantén las operaciones asíncronas pendientes y libera cada una en el orden sospechado. La aserción debe comprobar la invariante del sistema, no solo que se llamaron dos funciones.
Ejecuté el fixture de este artículo con Node.js v25.5.0. Usa node:test, Promises aplazadas y estado en memoria. El ejemplo demuestra una regla de actualización dentro de un proceso. No demuestra aislamiento de base de datos, un bloqueo distribuido, entrega de una cola ni el comportamiento de un navegador real.

Respuesta breve
- No uses
setTimeoutaleatorios para reproducir la carrera.- Crea Promises pendientes y controla cuándo termina cada operación.
- Escribe la invariante que debe seguir siendo cierta después de cada orden relevante.
- Repite la prueba en el límite real cuando el riesgo involucre una base de datos, una cola o un navegador.
¿Qué debe demostrar una prueba de condición de carrera?
Debe demostrar una afirmación sobre un orden posible. Por ejemplo, cuando un usuario hace dos búsquedas, la búsqueda más reciente puede actualizar la pantalla, pero una respuesta antigua no debe sobrescribirla después. La prueba debe forzar ese orden y revisar el estado final.
Una afirmación útil también dice lo que no puede ocurrir. "Dos Promises terminaron" describe la preparación. "El resultado antiguo no cambia el estado actual" describe la protección. Esta diferencia evita una prueba verde que nunca alcanza el efecto peligroso.
El runner de pruebas de Node.js, consultado el 29/09/2026, trata una Promise rechazada que devuelve la prueba como un fallo. Así puedes escribir un fixture asíncrono normal sin esconder el orden de ejecución detrás de otro callback.
¿Por qué Promise.all con un retraso suele fallar?
Promise.all espera todas las operaciones, pero no elige cuál termina primero. Un setTimeout puede hacer más probable un resultado, pero no convierte la secuencia en una prueba. El caso sigue dependiendo del reloj, de la carga de la máquina y del orden que el entorno decida usar.
Este patrón parece concurrente, pero no controla la carrera:
const result = await Promise.all([
load('first'),
load('second'),
]);
El código es correcto cuando solo necesitas esperar ambos resultados. No basta cuando la regla depende del orden de finalización. Repetir el caso en un bucle tampoco cierra la brecha. El bucle puede usar el mismo orden cientos de veces y nunca alcanzar la intercalación que causa el bug.
La guía de Doppler para probar condiciones de carrera de forma confiable, consultada el 29/09/2026, usa el mismo principio dentro de una transacción: la prueba retiene puntos intermedios hasta que las dos operaciones terminan la lectura necesaria. El detalle útil no es la base de datos concreta. Es convertir la secuencia en una entrada controlable de la prueba.
¿Cómo controlar la finalización de una Promise?
Una Promise aplazada expone su función resolve a la prueba. La operación comienza y queda detenida hasta que el caso libera el punto elegido. Así la prueba puede comenzar la búsqueda antigua, comenzar la nueva, resolver la nueva y solo después resolver la antigua.
function deferred() {
let resolve;
const promise = new Promise((value) => {
resolve = value;
});
return { promise, resolve };
}
Ahora crea una función que conserve solo el resultado de la solicitud más reciente. El contador es una versión local de la intención. Cada llamada captura su versión antes de esperar la lectura.
function createLatestLoader(read) {
let version = 0;
let current;
return {
load(key) {
const requestVersion = ++version;
return read(key).then((value) => {
if (requestVersion === version) current = value;
return value;
});
},
current() {
return current;
},
};
}
El guard no cancela la solicitud antigua. Evita que una finalización tardía cambie un estado que ya pertenece a una intención nueva. En otro sistema, la protección podría ser un AbortController, una versión persistida o una transacción. La invariante solo es la misma si el contrato del sistema lo establece.
¿Cómo escribir la prueba que reproduce el orden peligroso?
La prueba inicia las dos operaciones sin esperarlas de inmediato. Libera la segunda y espera a que termine. Por último, libera la primera y comprueba que el valor antiguo no sustituyó al nuevo.
import test from 'node:test';
import assert from 'node:assert/strict';
test('una respuesta tardía no sobrescribe el resultado más reciente', async () => {
const first = deferred();
const second = deferred();
const loader = createLatestLoader((key) =>
key === 'first' ? first.promise : second.promise,
);
const firstRun = loader.load('first');
const secondRun = loader.load('second');
second.resolve('new');
await secondRun;
first.resolve('old');
await firstRun;
assert.equal(loader.current(), 'new');
});
Ejecuté la lógica localmente con node --input-type=module y una versión equivalente enviada por la entrada estándar. En un archivo de prueba, el mismo caso puede ejecutarse con node --test. El resultado fue aprobado. Eso confirma el fixture y el guard para este orden. No es una medición de latencia ni demuestra que un servidor remoto haya cancelado trabajo.
Para comprobar que la prueba alcanza el defecto, elimina temporalmente if (requestVersion === version) y asigna siempre current = value. El mismo orden debe fallar porque la respuesta antigua termina al final y sobrescribe el valor nuevo. Si la prueba sigue verde al quitar la protección, nunca llegó a la línea importante.
¿Qué órdenes y fallos deben entrar en la matriz?
Empieza por el orden que motivó el bug, pero no termines ahí. Una matriz pequeña puede cubrir la primera operación antes de la segunda, la segunda antes de la primera, un fallo en la operación antigua y un fallo en la nueva. Cada escenario debe corresponder a una decisión observable.
| Escenario | Preparación | Invariante o resultado |
|---|---|---|
| la primera termina antes | libera first y después second |
el estado final sigue la regla del producto |
| la segunda termina antes | libera second y después first |
el resultado antiguo no sobrescribe el estado actual |
| falla la lectura antigua | rechaza first después de la segunda |
el fallo antiguo no borra el estado nuevo |
| falla la lectura nueva | rechaza second antes de la primera |
el sistema muestra error o fallback sin aceptar datos antiguos |
| cancelación | aborta la operación que ya no importa | la prueba distingue cancelación de un fallo inesperado |
No conviertas cada combinación en una prueba unitaria. La comparación entre pruebas unitarias y de integración en Node.js ayuda a decidir qué colaborador debe seguir siendo real. El fixture en memoria demuestra la decisión del controlador. La integración demuestra la serialización, la base de datos, la cola o el transporte que también participa en la carrera. Si la disputa involucra la misma intención de una API, prueba la idempotencia sin duplicar efectos en el límite real.
¿Los fake timers resuelven este problema?
A veces. Los fake timers sirven cuando el comportamiento depende de setTimeout, backoff, debounce o un intervalo. La documentación actual de Node.js sobre timers, consultada el 29/09/2026, describe las APIs de temporizadores y su variante basada en Promises. La guía para probar reintentos de JavaScript sin esperar el reloj real cubre ese límite.
Una carrera entre dos respuestas puede no depender del tiempo. Depende de cuál finalización cruza primero el siguiente paso. En ese caso, controlar las Promises comunica mejor la causa y evita una prueba que parece rápida, pero todavía oculta el orden.
Si la función programa un temporizador y también espera una Promise externa, controla ambas cosas por separado. Avanzar el reloj no resuelve automáticamente toda la cola de Promises, y resolver la Promise no dispara un temporizador que sigue pendiente. Haz explícito cada cambio de estado antes de la aserción.
¿Qué no demuestra el fixture en producción?
Un fixture determinista no sustituye una prueba en el límite donde la carrera causa daño. Un Map local no simula dos instancias, una transacción concurrente, la visibilidad de un mensaje ni la cancelación de una solicitud en el navegador.
Usa el fixture para demostrar el algoritmo y el contrato de estado. Después añade una prueba con el almacenamiento, broker, cliente HTTP o navegador que participa realmente en la operación. Registra qué parte quedó real y cuál siguió controlada, porque las dos pruebas responden preguntas diferentes.
Esa separación también evita una falsa sensación de cobertura. El artículo An Empirical Study of Flaky Tests in JavaScript, consultado el 29/09/2026, estudia la inestabilidad de pruebas en proyectos JavaScript. Es evidencia de que la confiabilidad de las pruebas es un problema real, no una prueba de que este fixture resuelva todas las carreras.
Preguntas frecuentes
¿Necesito ejecutar la prueba 1.000 veces?
No para demostrar un orden que puedes controlar. Libera las Promises aplazadas en la secuencia sospechada y haz una aserción determinista. La repetición puede complementar una prueba del entorno, pero no debe ser la única forma de alcanzar una intercalación concreta.
¿Las condiciones de carrera requieren varios hilos?
No. Dos operaciones asíncronas pueden intercalarse en sus puntos de espera y producir un estado incorrecto aunque JavaScript se ejecute en un solo hilo. La prueba debe representar la intercalación que importa, ya ocurra entre Promises, callbacks, solicitudes, mensajes o transacciones.
¿Debo cancelar la solicitud antigua o ignorar su respuesta?
Depende del contrato. Cancelar puede ahorrar trabajo cuando la dependencia respeta la señal, pero no deshace necesariamente un efecto que ya comenzó. Ignorar la respuesta protege el estado local. Prueba el efecto remoto por separado y documenta qué significa realmente cancelar.
Conclusión
Una condición de carrera deja de ser un accidente de timing cuando la prueba puede elegir el orden en que terminan las operaciones. Crea Promises aplazadas, fuerza la secuencia que amenaza la invariante y haz que el caso falle cuando desaparece el guard.
Después lleva la misma afirmación al límite real. Una base de datos, una cola, un navegador o un servicio remoto puede añadir reglas que un fixture en memoria no ve. El resultado es una suite honesta en dos partes: una prueba explica la lógica y otra comprueba el entorno que puede romperla.
Fuentes consultadas
- Node.js, Test runner, consultado el 2026-09-29.
- Node.js, Timers, consultado el 2026-09-29.
- Doppler, Reliably Testing Race Conditions, consultado el 2026-09-29. Se usa como referencia de discurso técnico para controlar puntos intermedios.
- arXiv, An Empirical Study of Flaky Tests in JavaScript, consultado el 2026-09-29.