mjmlio/mjml · error · Error
Circular inclusion detected on file : ${partialPath}
Error message
Circular inclusion detected on file : ${partialPath} What it means
handleInclude processes <mj-include path="..."> tags by reading the referenced partial. Before reading, it checks whether the partial's resolved path already appears in the current node's includedIn chain; if so, including it again would recurse forever, so it throws 'Circular inclusion detected on file : <path>'. The library detects include cycles at parse time rather than looping until stack overflow.
Source
Thrown at packages/mjml-parser-xml/src/index.js:235
decoded.includes('\0') ||
path.isAbsolute(decoded) ||
hasDriveLetter(decoded) ||
isUNCPath(decoded)
) {
denyInclude(line)
return
}
const partialPath = path.resolve(cwd, decoded)
const curBeforeInclude = cur
if (!isPathAllowed(partialPath)) {
denyInclude(line)
return
}
if (find(cur.includedIn, { file: partialPath }))
throw new Error(`Circular inclusion detected on file : ${partialPath}`)
let content
try {
content = fs.readFileSync(partialPath, 'utf8')
} catch (e) {
const newNode = {
line,
file,
absoluteFilePath: path.resolve(cwd, actualPath),
parent: cur,
tagName: 'mj-raw',
content: `<!-- mj-include fails to read file : ${file} at ${partialPath} -->`,
children: [],
errors: [
{
type: 'include',
params: { file, partialPath },View on GitHub (pinned to 6c01d35af5)
Solutions
- Inspect the cycle path in the message and break the loop: remove the <mj-include> that points back to an ancestor file.
- Restructure shared partials: extract common content into a leaf file both sides include instead.
- Grep included files for the offending file name to find all back-references.
- Ensure a self-referencing file no longer includes itself (file includes itself).
Example fix
<!-- before: a.mjml --> <mj-include path="./b.mjml" /> <!-- b.mjml: <mj-include path="./a.mjml" /> CIRCULAR --> <!-- after: b.mjml --> <mj-text>content</mj-text> <!-- remove include back to a.mjml -->
Defensive patterns
Strategy: validation
Validate before calling
const path = require('path');
function assertNoIncludeCycle(entry, resolve) {
const stack = new Set();
(function walk(f) {
if (stack.has(f)) throw new Error(`Circular include: ${f}`);
stack.add(f);
for (const inc of includesOf(f)) walk(resolve(path.dirname(f), inc));
stack.delete(f);
})(entry);
} Try / catch
try {
return MJMLParser(xml, opts)
} catch (e) {
if (e.message.startsWith('Circular inclusion detected')) {
console.error('Include cycle at:', e.message)
} else throw e
} Prevention
- Keep includes one-directional: partials never include their parents.
- Extract shared content into leaf partials.
- Add a static analysis test that walks mj-include graphs in CI.
When it happens
Trigger: Having a.mjml include b.mjml which includes a.mjml (direct cycle), or a longer cycle a->b->c->a, anywhere within <mj-include> tags parsed by MJMLParser (via parser -> handleInclude).
Common situations: Shared header/footer partials that include each other to pick up shared variables; refactor moving an include statement into a file that is itself included; accidental self-include of a file inside itself.
Related errors
AI-assisted analysis of mjmlio/mjml@6c01d35af5 (2026-09-02).
Data as JSON: /api/errors/f9791210aa12b438.
Report an issue: GitHub.