diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index 133a404ce7..a8b302b905 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -18,6 +18,8 @@ import { createServiceRef, createServiceFactory, coreServices, + createBackendPlugin, + createBackendModule, } from '@backstage/backend-plugin-api'; import { BackendInitializer } from './BackendInitializer'; import { ServiceRegistry } from './ServiceRegistry'; @@ -72,4 +74,45 @@ describe('BackendInitializer', () => { expect(rootFactory).toHaveBeenCalled(); expect(pluginFactory).not.toHaveBeenCalled(); }); + + it('should forward errors when plugins fail to start', async () => { + const init = new BackendInitializer(new ServiceRegistry([])); + init.add( + createBackendPlugin({ + pluginId: 'test', + register(reg) { + reg.registerInit({ + deps: {}, + async init() { + throw new Error('NOPE'); + }, + }); + }, + })(), + ); + await expect(init.start()).rejects.toThrow( + "Plugin 'test' startup failed; caused by Error: NOPE", + ); + }); + + it('should forward errors when modules fail to start', async () => { + const init = new BackendInitializer(new ServiceRegistry([])); + init.add( + createBackendModule({ + pluginId: 'test', + moduleId: 'mod', + register(reg) { + reg.registerInit({ + deps: {}, + async init() { + throw new Error('NOPE'); + }, + }); + }, + })(), + ); + await expect(init.start()).rejects.toThrow( + "Module 'mod' for plugin 'test' startup failed; caused by Error: NOPE", + ); + }); }); diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index f33254b025..8c2b75e887 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -26,6 +26,7 @@ import { EnumerableServiceHolder, ServiceOrExtensionPoint } from './types'; // Direct internal import to avoid duplication // eslint-disable-next-line @backstage/no-forbidden-package-imports import { InternalBackendFeature } from '@backstage/backend-plugin-api/src/wiring/types'; +import { ForwardedError } from '@backstage/errors'; export interface BackendRegisterInit { consumes: Set; @@ -194,12 +195,17 @@ export class BackendInitializer { // Modules are initialized before plugins, so that they can provide extension to the plugin const modules = moduleInits.get(pluginId) ?? []; await Promise.all( - Array.from(modules.values()).map(async moduleInit => { + Array.from(modules).map(async ([moduleId, moduleInit]) => { const moduleDeps = await this.#getInitDeps( moduleInit.init.deps, pluginId, ); - await moduleInit.init.func(moduleDeps); + await moduleInit.init.func(moduleDeps).catch(error => { + throw new ForwardedError( + `Module '${moduleId}' for plugin '${pluginId}' startup failed`, + error, + ); + }); }), ); @@ -211,7 +217,12 @@ export class BackendInitializer { pluginInit.init.deps, pluginId, ); - await pluginInit.init.func(pluginDeps); + await pluginInit.init.func(pluginDeps).catch(error => { + throw new ForwardedError( + `Plugin '${pluginId}' startup failed`, + error, + ); + }); } // Once the plugin and all modules have been initialized, we can signal that the plugin has stared up successfully