El contrato: Observer, Observable y Subscription
La gramática que gobierna todo stream: next, error y complete, la naturaleza perezosa del observable, y la suscripción como el momento en que el productor cobra vida y el consumidor gana el derecho a cancelar.
Un observable no es magia: es un contrato de tres piezas encajadas con precisión. El Observable es una función que describe cómo producir valores; el Observer es el trío de callbacks que los recibe; la Subscription es el hilo que conecta a ambos y que puedes cortar. Entre ellos rige una gramática inviolable —cero o más next, y a lo sumo un complete o un error— y una regla de oro: nada ocurre hasta que alguien se suscribe. Comprender este contrato es dejar de usar observables por imitación y empezar a razonarlos.
- Dominar los tres callbacks del Observer:
next,error,complete. - Entender la pereza (lazy) y por qué la suscripción dispara el productor.
- Manejar la
Subscriptiony la disciplina de la desuscripción. - Reconocer la gramática que ningún operador puede violar.
Las tres piezas del contrato
Reducido a su esencia, un observable es una función que recibe un observador y devuelve una forma de cancelar. Todo lo demás es azúcar. Esta es la anatomía sin librería alguna:
const reloj$ = new Observable<number>((subscriber) => {
let n = 0;
const id = setInterval(() => subscriber.next(n++), 1000);
// La función de teardown: se ejecuta al desuscribir o completar
return () => clearInterval(id);
});
const sub = reloj$.subscribe({
next: (v) => console.log('tick', v),
error: (e) => console.error('fallo', e),
complete: () => console.log('fin'),
});
El Observer es ese objeto con tres métodos. El Observable es la función productora que los invoca. La Subscription es el objeto sub que retorna subscribe, y que expone unsubscribe para detener todo y ejecutar la función de teardown. Tres piezas, una sola idea: describir la producción por separado del consumo. El observador puede ser parcial —si solo te interesan los valores, pasas una única función, y error y complete toman implementaciones por defecto—:
reloj$.subscribe((v) => console.log(v)); // observador abreviado: solo next
Conviene saber que ese error por defecto no es inocuo: si el stream falla y no proporcionaste un manejador, RxJS relanza el error de forma asíncrona para que no muera en silencio. Escuchar solo next es cómodo, pero te obliga a confiar en que la fuente nunca fallará.
La gramática inviolable
El contrato impone un orden que ningún operador ni fuente puede romper. Se expresa como una máquina de dos estados sobre las notificaciones:
stateDiagram-v2 [*] --> Activo Activo --> Activo: next Activo --> Cerrado: complete Activo --> Cerrado: error Cerrado --> [*]
En palabras: mientras el stream está activo puede emitir tantos next como quiera. En cualquier momento puede pasar a cerrado, y solo hay dos puertas de salida: complete (terminó bien) o error (terminó mal). Son mutuamente excluyentes y definitivos. Tras cualquiera de los dos, el observable no puede emitir nada más: ni un next tardío, ni un segundo complete. Esta garantía no es una cortesía, es estructural: el propio Subscriber de RxJS descarta cualquier notificación que llegue tras el cierre, así que aunque tu productor se equivoque y siga empujando, el observador no lo verá.
const s$ = new Observable<number>((sub) => {
sub.next(1);
sub.complete();
sub.next(2); // IGNORADO: llega tras complete, nadie lo recibe
});
Poder confiar en la gramática es lo que permite razonar composiciones enteras sin sorpresas: si construyes sobre un stream que la respeta, tu resultado también la respetará.
El error más caro de los principiantes es tratar error como un log que no detiene nada. No lo es: error cierra el stream para siempre. Si un observable de red falla, ese stream está muerto; los next posteriores no llegarán jamás. Para “sobrevivir” a un fallo no se ignora el error, se transforma el stream con operadores como catchError o retry, que producen un stream nuevo a partir del que murió. La resiliencia es composición, no supresión.
Pereza: nada ocurre sin suscripción
Aquí está la propiedad que más distingue al observable de la Promise. Crear un observable no ejecuta absolutamente nada; es una receta guardada, no un plato servido. La función productora solo corre cuando llega un subscribe. Esto tiene tres consecuencias que hay que interiorizar:
const peticion$ = from(fetch('/api/datos')); // NADA se ejecuta aún
peticion$.subscribe((d) => render(d)); // ahora sí: primera ejecución
peticion$.subscribe((d) => cache(d)); // OTRA ejecución completa: un segundo fetch
Primero, un observable sin suscriptores es inerte: no consume recursos, no lanza peticiones, no arranca timers. Segundo, cada suscripción es una ejecución independiente del productor —esto se llama ser “frío” (cold), y explica por qué el ejemplo dispara dos fetch—. Tercero, la pereza es lo que hace posible la cancelación: como el trabajo está atado a la suscripción, cortar la suscripción corta el trabajo. Una Promise, al ser ansiosa (eager), no ofrece ninguna de estas tres propiedades.
Este comportamiento por-suscripción se llama unicast: cada consumidor recibe su propia ejecución privada del productor, aislada de las demás. Es el modo por defecto y el más predecible, porque nada se comparte a menos que lo pidas de forma explícita.
La contraparte del observable frío es el caliente (hot): uno que produce con independencia de quién escuche, y reparte la misma emisión a todos —un websocket, un Subject—. Convertir uno frío en compartido es tarea del operador share, que multiplexa una sola ejecución entre varios suscriptores. Verás la familia caliente a fondo al estudiar RxJS y sus Subjects.
La Subscription y su disciplina
Si suscribirse arranca trabajo, no desuscribirse lo deja corriendo para siempre: esa es la fuga de memoria clásica de la programación reactiva. Un componente que se suscribe a un stream infinito y se destruye sin cancelar deja un productor emitiendo hacia el vacío, reteniendo closures y nodos del DOM ya muertos. La higiene es obligatoria, y hay dos estilos:
// Patron manual: agrupar suscripciones y cerrarlas juntas
const bag = new Subscription();
bag.add(clics$.subscribe(onClic));
bag.add(reloj$.subscribe(onTick));
bag.unsubscribe(); // al destruir el componente: cancela ambas de una vez
// Patron declarativo: dejar que un operador de cierre complete el stream
const destruido$ = new Subject<void>();
clics$.pipe(takeUntil(destruido$)).subscribe(onClic);
// destruido$.next() en el teardown hace complete y libera solo
En el ecosistema de 2026 la tendencia es no desuscribirse a mano. Angular ofrece takeUntilDestroyed, que ata el ciclo de vida del stream al del componente sin que declares un Subject auxiliar; el patrón general sigue siendo un operador de cierre —takeUntil, take, first— que hace que el propio stream complete cuando toca. Recordar que complete y error también liberan la suscripción es clave: un stream que termina limpia sus recursos sin que muevas un dedo. La regla mental: toda suscripción a una fuente potencialmente infinita necesita una condición de cierre explícita.
Para la limpieza que debe ocurrir pase lo que pase —cerrar un recurso, ocultar un indicador de carga— existe finalize, el equivalente reactivo de un bloque finally: se ejecuta una sola vez al terminar el stream por cualquier vía, sea complete, error o unsubscribe.
cargar$.pipe(
finalize(() => ocultarSpinner()), // corre en complete, error o unsubscribe
).subscribe({ next: pintar, error: mostrarFallo });
El contrato Observer/Observable/Subscription es, en el fondo, una separación radical entre describir un cómputo y ejecutarlo. El Observable describe: es un valor de primera clase que puedes pasar, almacenar, componer y transformar sin que ocurra ningún efecto. La suscripción ejecuta: es el único momento en que la descripción se convierte en acción, con un consumidor concreto y un punto de cancelación concreto. Esta separación es la misma que verás elevada a dogma en Elm, en la Composable Architecture de Swift y en cualquier sistema de efectos como valores: el efecto no se dispara al construirlo, sino que se entrega a un runtime que decide cuándo y cómo ejecutarlo, y que garantiza poder pararlo. Cuando entiendes que un observable no es “datos que llegan” sino “una receta perezosa y cancelable para producir datos”, dejas de pelearte con suscripciones fantasma y empiezas a diseñar flujos donde el ciclo de vida del trabajo coincide exactamente con el ciclo de vida de quien lo necesita. La gramática next/error/complete es el andamiaje que hace ese razonamiento demostrable en lugar de esperanzado.
- Implementa un
Observabledesde cero que emita tres valores connexty luegocomplete; verifica que unnexttrascompleteno llega al observador. - Añade una función de teardown y comprueba con un
console.logque se ejecuta tanto al completar como al desuscribir. - Crea un observable “frío” con un
fetchy suscríbete dos veces: observa en la pestaña de red que se lanzan dos peticiones. Luego aplicasharey repite. - Provoca una fuga: suscríbete a un
interval, destruye el contexto sin cancelar y explica qué queda vivo y por qué; después arréglalo contakeUntil.