markedjs/marked · error · Error

Infinite loop on byte: ${byte}

Error message

Infinite loop on byte: ${byte}

What it means

Thrown by the lexer's loop guard in blockTokens and inlineTokens. The lexer tracks the smallest src.length it has seen (srcLength); on each iteration, if src did not get strictly shorter than that minimum, marked assumes no tokenizer consumed any input and calls infiniteLoopError(src.charCodeAt(0)) (Lexer.ts:114-120, 337-343, 285-288, 473-476). The byte in the message is the char code of the first un-consumed character. This guard prevents a true infinite loop that would hang the process. With built-in tokenizers alone it is essentially unreachable because the text/inlineText tokenizers match at least one character; it almost always indicates a custom tokenizer extension that returned a token whose raw is empty (so src.substring(token.raw.length) leaves src unchanged), a zero-width regex match, or a start function that mis-clips src.

Source

Thrown at src/Lexer.ts:487

        }
        continue;
      }

      if (src) {
        this.infiniteLoopError(src.charCodeAt(0));
        break;
      }
    }

    return tokens;
  }

  private infiniteLoopError(byte: number) {
    const errMsg = 'Infinite loop on byte: ' + byte;
    if (this.options.silent) {
      console.error(errMsg);
    } else {
      throw new Error(errMsg);
    }
  }
}

View on GitHub (pinned to 9552b6bbca)

Solutions

  1. Audit every custom tokenizer extension: ensure the returned token's raw is the exact substring consumed and has length > 0.
  2. Make the tokenizer return undefined when it should NOT match, so built-in tokenizers get a chance to consume the byte.
  3. Reproduce with the offending input in isolation and log src.length per iteration to find which extension stalls.
  4. If using a start function, verify it returns a sane index (>= 0) and does not cause clipping that yields an empty match.
  5. Temporarily set silent:true to log instead of throw while debugging, but do not ship that.

Example fix

// before — raw is empty, src never shrinks => infinite loop
marked.use({
  extensions: [{
    name: 'tag',
    level: 'inline',
    tokenizer(src) {
      const m = /^\[\[(.*?)\]\]/.exec(src);
      if (!m) return;
      return { type: 'tag', raw: '', text: m[1], tokens: [] }; // BUG: raw should be m[0]
    },
    renderer(token) { return `<span>${token.text}</span>`; }
  }]
});

// after — raw is the full match, src advances
    return { type: 'tag', raw: m[0], text: m[1], tokens: [] };
Defensive patterns

Strategy: try-catch

Validate before calling

function assertExtensionConsumes(ext, samples) {
  for (const src of samples) {
    const tok = ext.tokenizer.call({ lexer: null }, src, []);
    if (tok && (!tok.raw || tok.raw.length === 0)) {
      throw new Error('extension "' + ext.name + '" matched but consumed 0 chars on input ' + JSON.stringify(src));
    }
  }
}
assertExtensionConsumes(myInlineExt, ['[[x]]', 'no match here', 'plain text']);

Type guard

function tokenConsumesInput(tok, srcBefore, srcAfter) {
  return !!tok && typeof tok.raw === 'string' && tok.raw.length > 0 && srcAfter.length < srcBefore.length;
}

Try / catch

try {
  return marked.parse(userInput);
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Infinite loop on byte:')) {
    logger.error({ byte: e.message, input: userInput }, 'marked infinite loop guard tripped');
    throw new Error('Unable to render markdown: tokenizer stalled');
  }
  throw e;
}

Prevention

When it happens

Trigger: A custom tokenizer extension whose returned token has raw:'' (or raw shorter than claimed) so src never shrinks (Lexer.ts:126, 355); an extension regex that matches zero-width (using * where + was needed); a start function that mis-clips src for the paragraph tokenizer yielding an empty match; a tokenizer that returns a token on some calls and undefined on others while built-in fallbacks also fail on that byte.

Common situations: Writing a tokenizer extension and forgetting to set raw (or setting it to ''); computing raw from the wrong capture group; an extension regex using * instead of + producing a zero-width match; porting an extension from an older marked where the contract differed.

Related errors


AI-assisted analysis of markedjs/marked@9552b6bbca (2026-08-13). Data as JSON: /api/errors/a6113eb15bf48abd. Report an issue: GitHub.