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
- Audit every custom tokenizer extension: ensure the returned token's raw is the exact substring consumed and has length > 0.
- Make the tokenizer return undefined when it should NOT match, so built-in tokenizers get a chance to consume the byte.
- Reproduce with the offending input in isolation and log src.length per iteration to find which extension stalls.
- If using a start function, verify it returns a sane index (>= 0) and does not cause clipping that yields an empty match.
- 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
- Always set token.raw to the exact matched substring and ensure its length > 0 before returning.
- Return undefined (not an empty token) when your extension does not match.
- Write a tokenizer-extension unit test that feeds tricky inputs and asserts src strictly shrinks on each successful call.
- Prefer + over * in extension regexes to avoid zero-width matches.
- Never retry the same input unchanged after this error - fix the extension or transform the input.
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
- extension level must be 'block' or 'inline'
- Token with "${token.type}" type was not found.
- extension name required
- tokenizer '${prop}' does not exist
AI-assisted analysis of markedjs/marked@9552b6bbca (2026-08-13).
Data as JSON: /api/errors/a6113eb15bf48abd.
Report an issue: GitHub.