mermaid-js/mermaid · error

State not found: ${trimmedId}

Error message

State not found: ${trimmedId}

What it means

Defensive guard inside addState(): in the branch where states.has(trimmedId) was true, the immediate states.get(trimmedId) is re-checked and throws if it returns undefined. In single-threaded use this is effectively unreachable (the map cannot lose an entry between has() and get()), so seeing it signals concurrent mutation of the internal state map or a corrupted/deleted entry mid-parse.

Source

Thrown at packages/mermaid/src/diagrams/state/stateDb.ts:414

  ) {
    const trimmedId = id?.trim();
    if (!this.currentDocument.states.has(trimmedId)) {
      log.info('Adding state ', trimmedId, descr);
      this.currentDocument.states.set(trimmedId, {
        stmt: STMT_STATE,
        id: trimmedId,
        descriptions: [],
        type,
        doc,
        note,
        classes: [],
        styles: [],
        textStyles: [],
      });
    } else {
      const state = this.currentDocument.states.get(trimmedId);
      if (!state) {
        throw new Error(`State not found: ${trimmedId}`);
      }
      if (!state.doc) {
        state.doc = doc;
      }
      if (!state.type) {
        state.type = type;
      }
    }

    if (descr) {
      log.info('Setting state description', trimmedId, descr);
      const descriptions = Array.isArray(descr) ? descr : [descr];
      descriptions.forEach((des) => this.addDescription(trimmedId, des.trim()));
    }

    if (note) {
      const doc2 = this.currentDocument.states.get(trimmedId);
      if (!doc2) {

View on GitHub (pinned to d93e9c88c0)

Solutions

  1. Give each render its own db instance (or call clear() before each parse) so the map is not shared.
  2. Avoid mutating the diagram db asynchronously while a parse is in flight.
  3. Audit custom parser hooks for any state deletion during addState.
  4. Reproduce in isolation; if it persists with a fresh db, report as a parser bug.

Example fix

// before — shared db reused concurrently
mermaid.render(d1); mermaid.render(d2); // races on stateDb

// after — isolate per render
await mermaid.render(id1, d1);
await mermaid.render(id2, d2);
Defensive patterns

Strategy: validation

Validate before calling

// Ensure the state exists before the second lookup style guards expect it
if (!stateDb.getDocument().states.has(trimmedId)) {
  throw new Error(`State ${trimmedId} must be declared first`);
}

Type guard

const isStateNotFound = (e): boolean =>
  e instanceof Error && /^State not found:/.test(e.message);

Try / catch

try {
  await mermaid.run({ nodes: [el] });
} catch (e) {
  if (e instanceof Error && /^State not found:/.test(e.message)) {
    // this is an internal invariant; isolate the db per render and retry
  } else { throw e; }
}

Prevention

When it happens

Trigger: The states Map is mutated from another context (shared db reused across diagrams without clear(), or async/worker access) so the entry present at has() is gone at get(); a custom parser extension that deletes states during traversal.

Common situations: Reusing a singleton stateDb across concurrently-rendered diagrams; clearing the db on one thread while another parses; forked/patched parser code that removes states; very rare race in an embedding host.

Related errors


AI-assisted analysis of mermaid-js/mermaid@d93e9c88c0 (2026-08-12). Data as JSON: /api/errors/39d0c0122a985f59. Report an issue: GitHub.