diff --git a/packages/e2e-tests/plugins/interactive-blocks/generator-scope/block.json b/packages/e2e-tests/plugins/interactive-blocks/generator-scope/block.json
new file mode 100644
index 00000000000000..32388d7a641d60
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/generator-scope/block.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "test/generator-scope",
+ "title": "E2E Interactivity tests - generator scope",
+ "category": "text",
+ "icon": "heart",
+ "description": "",
+ "supports": {
+ "interactivity": true
+ },
+ "textdomain": "e2e-interactivity",
+ "viewScriptModule": "file:./view.js",
+ "render": "file:./render.php"
+}
diff --git a/packages/e2e-tests/plugins/interactive-blocks/generator-scope/render.php b/packages/e2e-tests/plugins/interactive-blocks/generator-scope/render.php
new file mode 100644
index 00000000000000..c9991e441627d1
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/generator-scope/render.php
@@ -0,0 +1,16 @@
+
+
+
'' ) ); ?>
+>
+
+
+
+
diff --git a/packages/e2e-tests/plugins/interactive-blocks/generator-scope/view.asset.php b/packages/e2e-tests/plugins/interactive-blocks/generator-scope/view.asset.php
new file mode 100644
index 00000000000000..db23afdf657a19
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/generator-scope/view.asset.php
@@ -0,0 +1 @@
+ array( '@wordpress/interactivity' ) );
diff --git a/packages/e2e-tests/plugins/interactive-blocks/generator-scope/view.js b/packages/e2e-tests/plugins/interactive-blocks/generator-scope/view.js
new file mode 100644
index 00000000000000..26d526263cb462
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/generator-scope/view.js
@@ -0,0 +1,23 @@
+/**
+ * WordPress dependencies
+ */
+import { store, getContext } from '@wordpress/interactivity';
+
+store( 'test/generator-scope', {
+ callbacks: {
+ *resolve() {
+ try {
+ getContext().result = yield Promise.resolve( 'ok' );
+ } catch ( err ) {
+ getContext().result = err.toString();
+ }
+ },
+ *reject() {
+ try {
+ getContext().result = yield Promise.reject( new Error( '😘' ) );
+ } catch ( err ) {
+ getContext().result = err.toString();
+ }
+ },
+ },
+} );
diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md
index e54d34f518a580..1df9f0b7f1ec6e 100644
--- a/packages/interactivity/CHANGELOG.md
+++ b/packages/interactivity/CHANGELOG.md
@@ -1,6 +1,12 @@
-## Unreleased
+## 5.3.0 (2024-03-11)
+
+### Bug Fixes
+
+- Ensure scope is restored when catching exceptions thrown in async generator actions. ([#59708](https://github.com/WordPress/gutenberg/pull/59708))
+
+## 5.2.0 (2024-03-06)
### Bug Fixes
diff --git a/packages/interactivity/src/store.ts b/packages/interactivity/src/store.ts
index d160e2fd1c71f0..28600abd3c4dbc 100644
--- a/packages/interactivity/src/store.ts
+++ b/packages/interactivity/src/store.ts
@@ -109,7 +109,7 @@ const handlers = {
const scope = getScope();
const gen: Generator< any > = result( ...args );
- let value: any;
+ let value: unknown;
let it: IteratorResult< any >;
while ( true ) {
@@ -125,7 +125,12 @@ const handlers = {
try {
value = await it.value;
} catch ( e ) {
+ setNamespace( ns );
+ setScope( scope );
gen.throw( e );
+ } finally {
+ resetScope();
+ resetNamespace();
}
if ( it.done ) break;
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index 882eaa06b15e02..279ed611bf4bd6 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -77,6 +77,7 @@
/phpunit/*
+ *\.asset\.php$
/phpunit/*
diff --git a/test/e2e/specs/interactivity/async-actions.spec.ts b/test/e2e/specs/interactivity/async-actions.spec.ts
new file mode 100644
index 00000000000000..55e44d6cd4a085
--- /dev/null
+++ b/test/e2e/specs/interactivity/async-actions.spec.ts
@@ -0,0 +1,31 @@
+/**
+ * Internal dependencies
+ */
+import { test, expect } from './fixtures';
+
+test.describe( 'async actions', () => {
+ test.beforeAll( async ( { interactivityUtils: utils } ) => {
+ await utils.activatePlugins();
+ await utils.addPostWithBlock( 'test/generator-scope' );
+ } );
+ test.beforeEach( async ( { interactivityUtils: utils, page } ) => {
+ await page.goto( utils.getLink( 'test/generator-scope' ) );
+ } );
+ test.afterAll( async ( { interactivityUtils: utils } ) => {
+ await utils.deactivatePlugins();
+ await utils.deleteAllPosts();
+ } );
+
+ test( 'Promise generator callbacks should restore scope on resolve and reject', async ( {
+ page,
+ } ) => {
+ const resultInput = page.getByTestId( 'result' );
+ await expect( resultInput ).toHaveValue( '' );
+
+ await page.getByTestId( 'resolve' ).click();
+ await expect( resultInput ).toHaveValue( 'ok' );
+
+ await page.getByTestId( 'reject' ).click();
+ await expect( resultInput ).toHaveValue( 'Error: 😘' );
+ } );
+} );