skip to content

In an RxJS marble diagram string such as '--a--b--|', what do the dashes, the letters, | and # represent?

level: juniorimportance: should knowfreq 30%

answer

  1. time drawn as characters
  2. one dash, one frame
  3. letters are next values
  4. bar ends, hash fails
  5. parentheses share a frame

basics

~20 s

In RxJS marble diagrams each dash is one frame of virtual time, a letter or digit is a next value at that frame, | is completion and # is an error. In '--a--b--|', a emits at frame 2, b at 5, completion at 8.

solid answer

~50 s

A marble string draws one Observable's events on a virtual time line that starts at **frame zero**, the first character. Inside `TestScheduler.run` one frame is one virtual millisecond. `-` advances time by one frame; an alphanumeric character is a `next` value emitted on that frame (and it also advances time by one frame); `|` is `complete`; `#` is `error`. So `'--a--b--|'` emits `a` on frame 2, `b` on frame 5 and completes on frame 8. Parentheses group events into the **same** frame, as in `'(ab|)'`. Letters are placeholders: a `values` object such as `{ a: 1, b: 2 }` maps them to real values, and an optional third argument supplies the error value for `#`. In run mode, spaces are ignored and only help alignment, and time progression such as `'a 100ms b'` skips ahead without typing a hundred dashes.

go deeper

for a junior

Recall the alphabet: dash is a frame, letters are values, bar is complete, hash is error, parentheses group events on one frame.

for a middle

Explain frame arithmetic, including why values and groups advance time, and how values maps and time progression keep diagrams readable.

for a senior

Read failing marble output fluently, spot off-by-one-frame expectations, and write diagrams that line up visually for reviewers.

for a principal

Judge where marble tests pay off, typically time-dependent operator chains, versus plain value assertions that newcomers find easier to maintain.

## What a marble diagram is A **marble diagram** is a picture of an Observable's notifications over time. RxJS's `TestScheduler` (from `rxjs/testing`) turns an ASCII version of that picture into a test fixture or an expectation. Time runs left to right in **frames**; the first character of the string is **frame zero**. Inside `testScheduler.run(callback)` one frame equals **one virtual millisecond**. ## The characters | Character | Meaning | Advances time? | |---|---|---| | `-` | one frame passes, nothing happens | 1 frame | | `a`, `b`, `1` (any alphanumeric) | a `next` notification on this frame | 1 frame | | `\|` | `complete` on this frame | 1 frame | | `#` | `error` on this frame | 1 frame | | `(` ... `)` | a **synchronous group**: everything inside happens on the frame of `(` | the group's full length in characters | | space | ignored in run mode, used to line diagrams up | no | | `100ms`, `2s`, `1.5m` | **time progression**: skip ahead by that amount | that many frames | | `^` | subscription point (only in `hot()` diagrams and subscription marbles) | 1 frame | ## Reading examples - `'--a--b--|'`: `a` on frame 2, `b` on frame 5, completion on frame 8. - `'--a--b--#'`: the same values, then an error on frame 8. - `'-----(a|)'`: `a` and completion together on frame 5. - `'-'` or `'------'`: never emits, never completes, like `NEVER`. - `'|'`: completes immediately with no values, like `EMPTY`. - `'#'`: errors immediately, like a stream created with `throwError`. - `'a 9ms b'`: `a` on frame 0, `b` on frame 10. The value `a` itself advanced one frame, so 9 more reach frame 10. ## Mapping letters to real values The letters are placeholders. The helpers that consume a diagram take a `values` object (or array, indexed by digit) and an optional error value: ```ts import { TestScheduler } from 'rxjs/testing'; testScheduler.run(({ cold, expectObservable }) => { const source = cold('-a-b-#', { a: { id: 1 }, b: { id: 2 } }, new Error('boom')); expectObservable(source).toBe('-a-b-#', { a: { id: 1 }, b: { id: 2 } }, new Error('boom')); }); ``` If you leave out `values`, the character itself is the value (`'a'`), and if you leave out the error, `#` carries the string `'error'`. ## The two quirks everyone trips over 1. **Values take up a frame.** After `a` is emitted, time moves on by one frame. That is why `'a 9ms b'` puts `b` on frame 10, and why time progressions in expectations are often "one less" than the delay being tested. 2. **Groups take up their full width.** `'(abc)'` emits `a`, `b` and `c` on the frame where `(` stands, then advances time by **five** frames, the number of characters including the parentheses. So `'--(abc)-|'` completes on frame 8, not frame 4. The design helps vertical alignment but is a known pain point. ## Source diagrams and expected diagrams The same alphabet is used on both sides of a test. Inputs are built with `cold()` or `hot()`, and the expected output is written for `expectObservable(actual$).toBe(...)`. Because spaces are ignored in run mode, teams indent diagrams so that frames line up vertically: ```ts const source = cold('-a--b--c---|'); const expected = ' -a-----c---|'; ``` Read top to bottom, each column is one frame, so a reviewer can see that `b` was dropped without counting. Diagrams do not have to be the same length; trailing dashes after the last event simply mean time passes with nothing happening. A diagram without `|` or `#` describes a stream that is still open when the test ends, which is how you express "never completes" in an expectation. ## Why the notation is worth learning - Timing becomes visible: `'--a--b--|'` against `'-----b--|'` shows at a glance that a value was dropped. - Tests stay short: a debounce or delay scenario that would need several real timers fits on two lines. - Failures are precise: `TestScheduler` compares frame numbers and notifications, so an off-by-one frame is reported rather than hidden by a generous timeout. ## Run mode versus the legacy form Everything above describes diagrams used **inside** `testScheduler.run()`. Outside it, a frame is 10 virtual milliseconds, spaces count as frames, and time progression syntax is not supported. New tests should always use `run()`.

  • Why does '--(abc)-|' complete on frame 8 rather than frame 4?
    A synchronous group emits everything inside it on the frame of the opening parenthesis, but then advances time by the group's full character count, parentheses included. `(abc)` is five characters, so after two dashes and the group time is at frame 7, and the final dash puts completion on frame 8.
  • How do you make a marble diagram emit objects instead of single characters?
    Pass a `values` map as the second argument, for example `cold('-a-b|', { a: { id: 1 }, b: { id: 2 } })`, and the same map to `toBe()` for the expectation. The letters become keys, and the comparison uses the deep-equality function the `TestScheduler` was constructed with.

saying these in an interview costs you the question

  • Each dash in a marble string represents one real second.
  • A letter in a marble string does not advance time.
  • A group like (abc) occupies a single frame of time.
  • The # character marks completion and | marks an error.
  • Marble strings can only emit single-character strings as values.