diff --git a/packages/element/CHANGELOG.md b/packages/element/CHANGELOG.md
index 76dfb04c5d65c9..902f1c7fa5fe81 100644
--- a/packages/element/CHANGELOG.md
+++ b/packages/element/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+### Bug Fixes
+
+- `createInterpolateElement`: Warn and stop interpolating instead of throwing when an unmatched closing tag is present ([#60843](https://github.com/WordPress/gutenberg/issues/60843)).
+
## 8.3.0 (2026-07-14)
## 8.2.0 (2026-07-01)
diff --git a/packages/element/src/create-interpolate-element.ts b/packages/element/src/create-interpolate-element.ts
index 13763575c32faa..f75336e749c527 100644
--- a/packages/element/src/create-interpolate-element.ts
+++ b/packages/element/src/create-interpolate-element.ts
@@ -252,6 +252,19 @@ function proceed( conversionMap: Record< string, ReactElement > ): boolean {
return true;
case 'closer':
+ // A closing tag with no matching opening tag on the stack is
+ // invalid. Warn and bail, keeping anything interpolated so far.
+ if ( 0 === stackDepth ) {
+ if ( globalThis.SCRIPT_DEBUG ) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `Unmatched closing tag '${ name }>' in createInterpolateElement. The rest of the string was not interpolated.`
+ );
+ }
+ addText();
+ return false;
+ }
+
// If we're not nesting then this is easy - close the block.
if ( 1 === stackDepth ) {
closeOuterElement( startOffset );
diff --git a/packages/element/src/test/create-interpolate-element.tsx b/packages/element/src/test/create-interpolate-element.tsx
index 0bdb8c61f6243a..dbfa7a71160dd5 100644
--- a/packages/element/src/test/create-interpolate-element.tsx
+++ b/packages/element/src/test/create-interpolate-element.tsx
@@ -73,6 +73,38 @@ describe( 'createInterpolateElement', () => {
createInterpolateElement( testString, { 'spaced token': } )
).toEqual( expectedElement );
} );
+ it( 'warns and stops interpolating when there is an unmatched closing tag', () => {
+ const testString = 'This is a string';
+ const expectedElement = <>{ testString }>;
+ expect(
+ createInterpolateElement( testString, { item: } )
+ ).toEqual( expectedElement );
+ expect( console ).toHaveWarnedWith(
+ `Unmatched closing tag '' in createInterpolateElement. The rest of the string was not interpolated.`
+ );
+
+ // Tags matched before the unmatched closing tag stay interpolated, so
+ // only the remainder of the string is returned as text.
+ const partialString = 'This is a styled and string';
+ const partialExpectedElement = createElement(
+ Fragment,
+ null,
+ 'This is a ',
+ createElement( 'em', null, 'styled' ),
+ ' and string'
+ );
+ expect(
+ JSON.stringify(
+ createInterpolateElement( partialString, {
+ em: ,
+ strong: ,
+ } )
+ )
+ ).toEqual( JSON.stringify( partialExpectedElement ) );
+ expect( console ).toHaveWarnedWith(
+ `Unmatched closing tag '' in createInterpolateElement. The rest of the string was not interpolated.`
+ );
+ } );
it( 'returns expected react element for non nested components', () => {
const testString = 'This is a string with a link.';
const expectedElement = createElement(