The zip Operator Explained
The zip operator functions much like the zipper on a jacket—it couples corresponding elements from multiple observable streams into a single combined output. When given two or more source streams, it waits until each stream has produced a value at the same position (index), then merges these values into a tuple (or a pair for two streams) and passes the result downstream. If a projection function is supplied, it transforms the tuple before emission.
A key characteristic of zip is that it never emits a combined value until every source has delivered a fresh value for that particular index. Consequently, if one stream emits at a faster pace than another, the output rate is governed entirely by the slowest stream in the group.
The lifecycle of the resulting stream is tied to its sources. It sends a completion notification once any source completes and all remaining matched pairs have been emitted. Conversely, if a source never completes, the output also never completes. If any source throws an error, the error is forwarded to the subscriber.
The operational flow of zip can be broken down as follows:
- Subscribe to every input observable.
- On each emission from a source, store the value in a cache slot corresponding to that source's index.
- When all sources have a cached value for the same index, emit the combined result to the observer.
- Upon completion of any source (and after all possible pairs are delivered), send a complete notification.
- If any source errors, propagate that error notification.
The visual below illustrates how zip aligns two streams, A and B. The output emits only when a synchronized pair is available:

Consider the following code snippet, which replicates the scenario depicted in the diagram:
const a = stream('a', 200, 3);
const b = stream('b', 500, 3);
zip(a, b).subscribe(fullObserver(operator));
You can also experiment with it in this interactive demo:
