mermaid-js/mermaid · error · Error

Error: State name must be a single word. Found: "${yytext.tr

Error message

Error: State name must be a single word. Found: "${yytext.trim()}"

What it means

Thrown at stateDiagram.jison:133 by the lexer rule <STATE>\w+\s+\w+.*?\{ while the lexer is in the STATE state (pushed by the 'state' keyword rule at stateDiagram.jison:114). The rule matches a word, whitespace, another word, then anything up to an opening brace and rejects it, because a composite state (one with a {...} body) must use either a single-word identifier or a quoted label. A bare multi-word name before the brace is ambiguous with the ID/DESCR transitions and is therefore disallowed. The thrown value is a plain Error whose message echoes the offending yytext.

Source

Thrown at packages/mermaid/src/diagrams/state/parser/stateDiagram.jison:133

<STATE>.*"<<fork>>"                   {this.popState();yytext=yytext.slice(0,-8).trim(); /*console.warn('Fork Fork: ',yytext);*/return 'FORK';}
<STATE>.*"<<join>>"                   {this.popState();yytext=yytext.slice(0,-8).trim();/*console.warn('Fork Join: ',yytext);*/return 'JOIN';}
<STATE>.*"<<choice>>"                 {this.popState();yytext=yytext.slice(0,-10).trim();/*console.warn('Fork Join: ',yytext);*/return 'CHOICE';}
<STATE>.*"[[fork]]"                   {this.popState();yytext=yytext.slice(0,-8).trim();/*console.warn('Fork Fork: ',yytext);*/return 'FORK';}
<STATE>.*"[[join]]"                   {this.popState();yytext=yytext.slice(0,-8).trim();/*console.warn('Fork Join: ',yytext);*/return 'JOIN';}
<STATE>.*"[[choice]]"                 {this.popState();yytext=yytext.slice(0,-10).trim();/*console.warn('Fork Join: ',yytext);*/return 'CHOICE';}

<struct>.*direction\s+TB[^\n]*            { return 'direction_tb';}
<struct>.*direction\s+BT[^\n]*            { return 'direction_bt';}
<struct>.*direction\s+RL[^\n]*            { return 'direction_rl';}
<struct>.*direction\s+LR[^\n]*            { return 'direction_lr';}

<STATE>["]                 { /* console.log('Starting STATE_STRING'); */ this.pushState("STATE_STRING"); }
<STATE>\s*"as"\s+          { this.pushState('STATE_ID'); /* console.log('pushState(STATE_ID)'); */ return "AS"; }
<STATE_ID>[^\n\{]*         { if (!processId()) return; this.popState(); /* console.log('STATE_ID', yytext); */ return "ID"; }
<STATE_STRING>["]          { this.popState(); }
<STATE_STRING>[^"]*        { /* console.log('Long description:', yytext); */ return "STATE_DESCR"; }
<STATE>\w+\s+\w+.*?\{      { throw new Error('Error: State name must be a single word. Found: "' + yytext.trim() + '"'); }
<STATE>[^\n\s\{]+          { /* console.log('COMPOSIT_STATE', yytext); */ return 'COMPOSIT_STATE'; }
<STATE>\n                  { this.popState(); }
<INITIAL,STATE>\{          { this.popState(); this.pushState('struct'); /* console.log('begin struct', yytext); */ return 'STRUCT_START'; }
<struct>\}                 { /*console.log('Ending struct');*/ this.popState(); return 'STRUCT_STOP';} }
<struct>[\n]               /* nothing */

<INITIAL,struct>"note"\s+           { this.begin('NOTE'); return 'note'; }
<NOTE>"left of"                     { this.popState(); this.pushState('NOTE_ID'); return 'left_of'; }
<NOTE>"right of"                    { this.popState(); this.pushState('NOTE_ID'); return 'right_of'; }
<NOTE>\"                            { this.popState(); this.pushState('FLOATING_NOTE'); }
<FLOATING_NOTE>\s*"as"\s*           { this.popState(); this.pushState('FLOATING_NOTE_ID'); return "AS"; }
<FLOATING_NOTE>["]                  /**/
<FLOATING_NOTE>[^"]*                { /* console.log('Floating note text: ', yytext); */ return "NOTE_TEXT"; }
<FLOATING_NOTE_ID>[^\n]*            { if (!processId()) return; this.popState(); /* console.log('Floating note ID', yytext);*/ return "ID"; }
<NOTE_ID>\s*[^:\n\s\-]+             { if (!processId()) return; this.popState(); this.pushState('NOTE_TEXT'); /*console.log('Got ID for note', yytext);*/ return 'ID'; }
<NOTE_TEXT>\s*":"[^:\n;]+           { this.popState(); /* console.log('Got NOTE_TEXT for note',yytext);*/yytext = yytext.substr(2).trim(); return 'NOTE_TEXT'; }
<NOTE_TEXT>[\s\S]*?\n\s*"end note"  { this.popState(); /* console.log('Got NOTE_TEXT for note',yytext);*/yytext = yytext.slice(0,-8).trim(); return 'NOTE_TEXT'; }

View on GitHub (pinned to d93e9c88c0)

Solutions

  1. Quote the multi-word label: state "Foo Bar" { ... }
  2. Use an explicit alias: state "Foo Bar" as FB { ... }
  3. Use a single-word identifier with no space: state FooBar { ... }

Example fix

// before
stateDiagram-v2
  state Foo Bar {
    [*] --> Bar
  }

// after
stateDiagram-v2
  state "Foo Bar" {
    [*] --> Bar
  }
Defensive patterns

Strategy: validation

Validate before calling

// Catches the 'state <word> <word> ... {' shape before the lexer throws.
function validateCompositeStateNames(src) {
  for (const line of src.split('\n')) {
    if (/^\s*state\s+\w+\s+\w+.*\{/.test(line)) {
      throw new Error('Composite state name must be quoted or single-word: ' + line.trim());
    }
  }
}

Type guard

function isStateNameError(e) { return e instanceof Error && /State name must be a single word/.test(e.message); }

Try / catch

try {
  stateParser.parse(src);
} catch (e) {
  if (e instanceof Error && /State name must be a single word/.test(e.message)) {
    // prompt the user to quote multi-word composite state names
  } else throw e;
}

Prevention

When it happens

Trigger: A stateDiagram or stateDiagram-v2 source containing a composite state whose name has a space and is neither quoted nor given an 'as' alias, e.g. 'state Foo Bar { ... }'. The lexer is in STATE mode when it matches word-space-word-...-{ and throws immediately during lexing, before the grammar reduces.

Common situations: Authoring a nested/composite state with a descriptive multi-word label and forgetting the quotes; assuming the simple-ID rule allows spaces; migrating from another tool's syntax that permitted spaces in state names; confusing the display label with the internal identifier.

Related errors


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