wandres.dev
QUÉ ES EL ESTADO · la raíz de la complejidad

Estado compartido y mutable

Si el estado es la raíz de la complejidad, el estado compartido y mutable es la raíz de los bugs. Son dos palabras que por separado se manejan y juntas se vuelven tóxicas: compartido —más de un dueño— y mutable —puede cambiar—. Aquí se diseccionan las tres formas en que esa pareja envenena un programa (el aliasing, la dependencia del orden y las condiciones de carrera lógicas) y se muestra que casi todo mecanismo de control de concurrencia es una defensa contra la misma pareja.

⏱ 16 min

Si el estado es la raíz de la complejidad, el estado compartido y mutable es la raíz de los bugs. Fíjate en que hacen falta las dos palabras. Un dato inmutable puede compartirlo el mundo entero sin peligro; un dato mutable que nadie más ve tampoco molesta. El veneno está en la intersección: cuando algo puede cambiar y, a la vez, más de uno lo observa. Ahí viven el aliasing, la dependencia del orden y las condiciones de carrera lógicas.

🎯 Al terminar esta lección sabrás
  • Entender por qué la combinación compartido más mutable, y no cada término por separado, es el problema.
  • Reconocer el aliasing: dos nombres para el mismo objeto y las mutaciones a distancia.
  • Ver cómo la dependencia del orden hace que el resultado dependa de la secuencia de operaciones.
  • Identificar condiciones de carrera lógicas incluso en código de un solo hilo.

La combinación tóxica

Aíslalo en una tabla mental de dos ejes: compartido o no, mutable o no. Un dato compartido e inmutable es seguro: todos lo leen y nadie lo rompe —es el modelo de la programación funcional y de las constantes—. Un dato mutable y no compartido también es seguro: hay un solo dueño, así que ninguna sorpresa viene de fuera —es una variable local—. El único cuadrante peligroso es el cuarto: compartido y mutable a la vez.

Que sean exactamente dos ejes, y no una lista difusa de malas prácticas, es lo que hace la idea tan potente: reduce un problema que parecía infinito —¿por qué fallan los programas concurrentes?— a una única casilla de una tabla de cuatro. Todo lo demás del capítulo es explorar esa casilla y catalogar las formas en que hace daño.

🧊

Compartido + inmutable

Seguro. Config, constantes, valores congelados. Todos leen; nada cambia bajo tus pies. Puedes repartir referencias sin miedo.

📦

Mutable + aislado

Seguro. Una variable local, un acumulador de un bucle. Un solo dueño y ninguna sorpresa externa: nadie más puede tocarlo.

☠️

Compartido + mutable

Peligro. Aquí viven el aliasing, los órdenes frágiles y las carreras. Es el único cuadrante que hay que domar, y donde casi todo bug de estado nace.

🎯

La lección

No elimines la mutación ni el compartir por dogma. Basta con romper la coincidencia de ambos sobre el mismo dato en el mismo instante.

Esta es la clave que unifica media docena de tecnologías. No hay que prohibir la mutación (aunque los lenguajes funcionales lo hacen) ni prohibir el compartir (aunque los actores lo hacen): basta con impedir que ocurran a la vez sobre el mismo dato. Cada familia de soluciones ataca uno de los dos términos, y verlo así convierte un zoo de herramientas en variaciones de una sola idea.

Conviene subrayar por qué cada término, a solas, es inofensivo, porque de ahí salen las dos grandes estrategias. Si prohíbes la mutación, puedes compartir sin límite: es el camino funcional, el de la inmutabilidad del nivel 7. Si prohíbes el compartir, puedes mutar con libertad: es el camino de los actores y de la encapsulación estricta. Ambos funcionan porque rompen la coincidencia, y cada uno elige sacrificar el término que su problema tolere mejor.

📝
Ni mutar ni compartir son el enemigo

Es un error frecuente concluir de todo esto que la mutación es mala, o que el estado global es malo. No lo son en sí mismos: una variable local muta felizmente, y una constante se comparte sin riesgo alguno. Lo que hay que perseguir no es ninguno de los dos por separado, sino su intersección. Cazar mutación inocente o compartir inocente es gastar disciplina donde no hay peligro; el ojo entrenado apunta solo al cuadrante donde ambos se tocan.

Aliasing: dos nombres, un objeto

En JavaScript, como en casi todo lenguaje con objetos, las variables no guardan objetos: guardan referencias. const b = a no copia nada; crea un segundo nombre para el mismo objeto. Mutar b.x muta a.x, porque a y b son etiquetas pegadas a la misma caja. A esto se le llama aliasing, y es la forma más silenciosa del estado compartido.

const config = { tema: "oscuro", idioma: "es" };
const copia = config;      // NO es una copia: es otro nombre del mismo objeto
copia.tema = "claro";
config.tema;               // "claro": mutaste algo que creías intacto

El bug clásico nace cuando una función recibe un objeto y lo muta. Quien la llamó no esperaba que su objeto cambiara —lo pasó para que lo leyeran, no para que lo reescribieran— y ahora tiene un valor distinto sin haber hecho nada. Es “acción fantasma a distancia”: el efecto ocurre lejos de la causa, en otro archivo, y en el punto donde estalla el bug no hay ninguna pista de quién lo provocó.

function activar(opciones: { activo: boolean }): void {
  opciones.activo = true; // muta el objeto del llamador, no una copia local
}
const base = { activo: false };
activar(base);
base.activo; // true: la funcion cambio algo que el llamador creia intacto

Nada en la firma de activar avisa de que va a escribir en su argumento. Quien la usa asume que pasar un objeto es prestarlo para leer, no cederlo para reescribir. Por eso las guías de estilo insisten en tratar los parámetros como inmutables y en devolver objetos nuevos en vez de mutar los recibidos: es la convención que suple la ausencia de una regla del lenguaje. La inmutabilidad del nivel 7 existe para cerrar esta grieta de raíz: si nadie puede mutar, el aliasing deja de doler.

ℹ️
Copiar de verdad

Para romper un alias hay que copiar el objeto, no la variable. Una copia superficial ({ ...config }) basta si el objeto es plano; para estructuras anidadas necesitas una copia profunda (structuredClone(config)) o, mejor, datos inmutables que hagan la copia innecesaria. Reconocer cuándo tienes un alias y no una copia es la mitad de los bugs de mutación resueltos antes de que ocurran.

Rust convirtió esta intuición en una regla del compilador: en cada instante, un dato puede tener muchas referencias inmutables o una sola mutable, nunca ambas. Es literalmente aliasing xor mutabilidad, verificado antes de ejecutar. No es celo gratuito: es el reconocimiento, elevado a tipo, de que la pareja compartido-más-mutable es la fuente del daño, y de que basta prohibir su coincidencia para borrar clases enteras de errores.

En los lenguajes que no tienen esa regla —JavaScript, Python, casi todos—, la garantía no la da el compilador sino tú. De ahí que la inmutabilidad, las copias defensivas y los datos congelados no sean manías de puristas, sino la forma de recrear a mano la protección que Rust obtiene gratis. Todo el nivel 7 es, en el fondo, reconstruir esa regla con disciplina allí donde el lenguaje no la impone.

Orden y carreras: cuando la secuencia decide

El segundo veneno es la dependencia del orden. Con estado compartido y mutable, a(); b(); puede dar un resultado distinto de b(); a();, porque cada uno lee y escribe el estado que el otro dejó. El significado del programa deja de estar en cada línea y pasa a estar en la secuencia, que es mucho más difícil de sostener en la cabeza y trivial de romper al reordenar código “inocentemente”.

De ahí a las condiciones de carrera hay un paso, y contra la intuición común no hacen falta hilos: basta la asincronía. Imagina dos manejadores que leen un contador, lo incrementan y lo guardan; si ambos leen antes de que cualquiera escriba, uno de los incrementos se pierde. En JavaScript de un solo hilo esto ocurre cada vez que hay un await entre la lectura y la escritura: el intercalado de dos tareas asíncronas produce una carrera lógica, sin paralelismo real.

let contador = 0;
async function incrementar() {
  const actual = contador;      // lee
  await guardarEnDisco(actual); // cede el turno: otra tarea puede intercalarse
  contador = actual + 1;        // escribe sobre un valor quiza ya obsoleto
}
await Promise.all([incrementar(), incrementar()]); // esperado 2, real a veces 1

Ningún hilo corre en paralelo aquí: es un solo hilo con dos tareas que se turnan en cada await. Basta con que la segunda lea antes de que la primera escriba para que un incremento se pierda. La carrera no vive en el paralelismo, sino en la ventana abierta entre leer y escribir un estado compartido.

flowchart TD
I[contador vale 0] --> A[tarea A lee 0]
I --> B[tarea B lee 0]
A --> A2[A escribe 1]
B --> B2[B escribe 1]
A2 --> R[resultado final 1]
B2 --> R
R --> P[se perdio un incremento]
style I fill:#89b4fa,color:#11111b
style P fill:#f38ba8,color:#11111b

El caso más habitual en frontend es el “last write wins” de los efectos: dos peticiones lanzadas en orden, la primera responde después de la segunda, y su resultado tardío pisa al bueno. El usuario buscó “b” pero ve resultados de “a” porque “a” tardó más. Es una carrera lógica pura: el bug no está en ninguna línea, sino en qué respuesta llegó última —algo que no controlas—.

La cura en frontend moderno es tratar la última petición como la única fuente válida: cancelar las anteriores con AbortController, o ignorar toda respuesta que no corresponda a la búsqueda vigente. TanStack Query lo hace por ti precisamente porque el problema es universal. Fíjate en la forma de la solución: designar una verdad —la petición actual— y descartar las demás. Es, un capítulo antes de tiempo, la fuente única de verdad asomando la cabeza.

Tres venenos, un mismo origen

Los tres males de este capítulo no son independientes: son tres síntomas de la misma enfermedad, el estado compartido que además muta. El aliasing es compartir sin saberlo; la dependencia del orden es compartir a través del tiempo; la carrera es compartir a través de la asincronía. Cambia el eje —espacio, secuencia, intercalado—, pero la causa es una sola.

Veneno Qué es Antídoto habitual
Aliasing Dos nombres para un objeto mutable Inmutabilidad o copiar al escribir
Orden frágil El resultado depende de la secuencia Operaciones puras y conmutativas
Carrera lógica El intercalado decide el valor final Un único dueño que serializa la escritura

Mira la última columna: cada antídoto elimina la mutación o elimina el compartir. No hay un tercer camino, porque no hay una tercera causa. Esa es la observación que la síntesis siguiente lleva hasta su conclusión.

Toda la concurrencia es una respuesta a la misma pareja

Detente a mirar el catálogo de mecanismos que la industria inventó para el estado concurrente y verás que todos atacan la misma pareja tóxica desde ángulos distintos. Los cerrojos serializan el acceso: permiten compartir y mutar, pero nunca a la vez, imponiendo turnos. Los lenguajes funcionales eliminan la mutación: comparte cuanto quieras, porque nada cambia. El modelo de actores elimina el compartir: cada actor muta solo su estado y los demás le hablan por mensajes. Redux serializa la mutación en un único flujo ordenado de acciones, de modo que el orden deja de ser ambiguo. Las transacciones dan atomicidad: la ilusión de que tu secuencia ocurrió sin intercalarse. Rust lo vuelve un tipo: aliasing xor mutación, comprobado por el compilador. Cinco culturas de programación, cinco décadas de investigación y una sola diana: impedir que compartido y mutable coincidan sobre el mismo dato en el mismo instante. Cuando interiorizas que ese es el enemigo, dejas de memorizar APIs de concurrencia y empiezas a leer cada una como lo que es —una manera distinta de romper la pareja— y a elegir según cuál de los dos términos te conviene sacrificar en tu problema.

⚔️ Provoca el veneno a mano
  1. Escribe const b = a sobre un objeto, muta b y comprueba que a cambió. Ahora rómpelo con una copia (structuredClone o { ...a }) y observa la diferencia.
  2. Escribe una función que reciba un objeto y lo mute. Llámala y verifica que el objeto del llamador cambió sin su permiso. ¿Cómo lo evitarías sin copiar de más?
  3. Reproduce una carrera lógica: dos async que leen un contador, esperan un await y lo escriben. Confirma que se pierde un incremento.
  4. Clasifica el estado de un módulo tuyo en los cuatro cuadrantes. Para cada dato del cuadrante tóxico, decide qué término romperías: ¿lo harías inmutable, o le darías un solo dueño?