Commit a949bc83aa4 for woocommerce
commit a949bc83aa413932877fdbf203dc4ffe2b3031e1
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date: Thu Aug 27 17:29:31 2026 +0300
Dispatch thrown plugin errors as error notices in createNoticesFromResponse (#68095)
fix(notices): dispatch thrown errors as error notices
createNoticesFromResponse() was written for REST response shapes: it
loops over `errors`, and otherwise picks the notice status from the
presence of `code`. The plugin actions in @woocommerce/data reject with
a PluginError, an Error subclass carrying a message and per-plugin data
but no code, and around seventeen client/admin callers hand that
rejection straight to the helper. It fell into the generic branch and
dispatched the failure to core/notices with status `success`: right
text, wrong status.
No surface renders that difference today. wc-admin's only consumer of
core/notices is TransientNotices -> SnackbarList -> Snackbar, which
drops `status` and hard-codes a polite announcement, as upstream's
Snackbar does. So this corrects the data written to a shared store
rather than anything a merchant currently sees. It stays worth fixing
because the moment any surface reads `status`, every plugin install
failure reads as a success.
Add an `instanceof Error` branch ahead of the generic one that always
dispatches `error`. It sits after the existing TypeError/offline check
so that copy is unchanged, and no REST response object is an Error, so
every existing branch keeps its behaviour. Fixing the helper covers all
current callers, and future ones, without touching them.
Refs #68092
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
diff --git a/plugins/woocommerce/changelog/fix-68092-error-notices-from-plugin-error b/plugins/woocommerce/changelog/fix-68092-error-notices-from-plugin-error
new file mode 100644
index 00000000000..588db584f51
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-68092-error-notices-from-plugin-error
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Dispatch plugin install/activate failures as error notices instead of success notices when the rejected PluginError is passed to createNoticesFromResponse().
diff --git a/plugins/woocommerce/client/admin/client/lib/notices/index.js b/plugins/woocommerce/client/admin/client/lib/notices/index.js
index 62c87778251..3b99c59603d 100644
--- a/plugins/woocommerce/client/admin/client/lib/notices/index.js
+++ b/plugins/woocommerce/client/admin/client/lib/notices/index.js
@@ -67,6 +67,12 @@ export function createNoticesFromResponse( response ) {
Object.keys( response.errors ).forEach( ( errorKey ) => {
createNotice( 'error', response.errors[ errorKey ].join( ' ' ) );
} );
+ } else if ( response instanceof Error ) {
+ // A thrown Error (e.g. the PluginError rejected by @woocommerce/data's
+ // plugin actions) is always a failure, even though it carries no code.
+ if ( response.message ) {
+ createNotice( 'error', response.message );
+ }
} else if ( response.message ) {
// Handle generic messages.
createNotice( response.code ? 'error' : 'success', response.message );
diff --git a/plugins/woocommerce/client/admin/client/lib/notices/test/index.js b/plugins/woocommerce/client/admin/client/lib/notices/test/index.js
index 1620722e1cd..7c4311f516d 100644
--- a/plugins/woocommerce/client/admin/client/lib/notices/test/index.js
+++ b/plugins/woocommerce/client/admin/client/lib/notices/test/index.js
@@ -81,6 +81,35 @@ describe( 'createNoticesFromResponse', () => {
expect( call2 ).toEqual( [ 'error', response.errors.item2[ 0 ] ] );
} );
+ test( 'should create an error notice when the response is a thrown Error', () => {
+ // Mirrors the PluginError thrown by @woocommerce/data's plugin actions: an
+ // Error subclass carrying a message and data, but no code.
+ class PluginError extends Error {
+ constructor( message, data ) {
+ super( message );
+ this.data = data;
+ }
+ }
+ const response = new PluginError( 'Could not install foo. Reason.', {
+ foo: [ 'Reason.' ],
+ } );
+
+ createNoticesFromResponse( response );
+ expect( createNotice ).toHaveBeenCalledWith(
+ 'error',
+ response.message
+ );
+ expect( createNotice ).toHaveBeenCalledTimes( 1 );
+ } );
+
+ test( 'should not create an empty error notice when an Error has no message', () => {
+ // Pins the message guard inside the Error branch: an Error inherits an
+ // empty `message` from the prototype, so without the guard this would
+ // dispatch a notice with no text.
+ createNoticesFromResponse( new Error() );
+ expect( createNotice ).not.toHaveBeenCalled();
+ } );
+
test( 'should not call createNotice when no message or errors exist', () => {
const response = { data: {} };