remotion-dev/remotion · error · Error
Expected a printed self-closing JSX element
Error message
Expected a printed self-closing JSX element
What it means
In Remotion Studio codemods, printJsxOpeningElement reprints a JSX element and, when the source element was NOT self-closing, converts the printed output into a self-closing form by locating the trailing '/>'. If the printed output does not end with '/>', the transformation invariant is broken and the library throws instead of producing corrupted source. This is an internal formatting invariant, not a user-input validation.
Solutions
- Ensure you remove/clear the element's children (element.children = []) before calling printJsxOpeningElement so the printer emits a self-closing tag.
- Check the codemod bookkeeping: wasSelfClosing must match the actual AST node; if the source element was self-closing, the function returns early without this check.
- Verify no custom recast printer/prettier plugin rewrites JSX output; test with default printing options.
- If you hit this after upgrading recast/prettier, pin known-working versions and reproduce with a minimal AST to file an issue.
Example fix
// before: element still has children so print ends with '</Video>' const printed = printJsxOpeningElement(path); // after path.node.children = []; const printed = printJsxOpeningElement(path); // now ends with '/>'
Defensive patterns
Strategy: validation
Validate before calling
const printed = recast.print(path.node).code;
if (!printed.trimEnd().endsWith('/>')) {
throw new Error('Element still renders with children; clear children before printing');
} Type guard
const isSelfClosing = (node: t.JSXElement) => node.openingElement.selfClosing && node.children.length === 0;
Try / catch
try {
const printed = printJsxOpeningElement(path);
} catch (err) {
if ((err as Error).message.includes('self-closing JSX')) {
path.node.children = [];
}
} Prevention
- Clear node.children before requesting a self-closing print
- Keep wasSelfClosing bookkeeping in sync with the actual AST
- Test codemods against default recast/prettier printing settings
- Pin recast and prettier versions in the codemod package
When it happens
Trigger: Calling printJsxOpeningElement (via a codemod that removes or rewrites children of a JSX element) when recast's generated output unexpectedly ends with children instead of a self-closing tag, e.g. after astTypes/printer customizations or when the AST has been mutated to still contain a children node while wasSelfClosing bookkeeping said otherwise.
Common situations: Developers writing a custom Studio codemod that mutates JSXElement children before calling this helper, or running against an unusual prettier/recast version whose JSX printer emits a different formatting (e.g. children on separate lines), so the printed string no longer ends with '/>'.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- Cannot duplicate JSX element with no parent
- Cannot split a JSX sequence without a source location
- Cannot split JSX sequence with no parent
- Could not find a root JSX element
- Could not find a root JSX element
AI-assisted analysis of remotion-dev/remotion@46a3a6bf13 (2026-09-18).
Data as JSON: /api/errors/55cd79204d5e05bb.
Report an issue: GitHub.
Appendix: source
Thrown at packages/studio-codemods/src/print-jsx.ts:321
[],
);
const printed = printInsertedJsx({
element: printableElement,
input,
originalAttributeSources,
prettierConfigOverride: {
bracketSpacing: getObjectCurlySpacing(input, prettierConfigOverride),
singleQuote:
getPreferredQuote(input, prettierConfigOverride) === 'single',
...prettierConfigOverride,
},
});
if (wasSelfClosing) {
return printed;
}
if (!printed.endsWith('/>')) {
throw new Error('Expected a printed self-closing JSX element');
}
const slashIndex = printed.length - 2;
const lastLineStart = printed.lastIndexOf('\n') + 1;
const beforeSlash = printed.slice(lastLineStart, slashIndex);
return beforeSlash.trim().length === 0
? `${printed.slice(0, slashIndex)}>`
: `${printed.slice(0, slashIndex).trimEnd()}>`;
};
View on GitHub (pinned to 46a3a6bf13)