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

  1. Ensure you remove/clear the element's children (element.children = []) before calling printJsxOpeningElement so the printer emits a self-closing tag.
  2. 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.
  3. Verify no custom recast printer/prettier plugin rewrites JSX output; test with default printing options.
  4. 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

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


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)