From b2319ffe45441f5f3693a4f30fc62f7e94b1f5c5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Apr 2026 01:33:34 +0200 Subject: [PATCH] errors: add `toError` utility and migrate `assertError` usages Add a `toError` utility function to `@backstage/errors` that converts unknown values to `ErrorLike` objects. If the value is already error-like it is returned as-is. Strings are used directly as the error message, and other values are stringified with a fallback to JSON.stringify to avoid unhelpful `[object Object]` messages. Non-error causes passed to `CustomErrorBase` are now converted and stored using `toError` rather than discarded. Existing `assertError` call sites across the codebase are migrated to `toError`. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/add-to-error-utility.md | 5 ++ .../src/wiring/BackendInitializer.ts | 22 ++--- .../auth/plugin/PluginTokenHandler.ts | 8 +- .../http/applyInternalErrorFilter.ts | 9 +- .../urlReader/lib/AwsCodeCommitUrlReader.ts | 10 +-- .../urlReader/lib/AwsS3UrlReader.ts | 10 +-- .../lib/AzureBlobStorageUrlReader.ts | 11 +-- .../urlReader/lib/AzureUrlReader.ts | 10 +-- .../urlReader/lib/BitbucketCloudUrlReader.ts | 10 +-- .../urlReader/lib/BitbucketServerUrlReader.ts | 10 +-- .../urlReader/lib/FetchUrlReader.ts | 10 +-- .../urlReader/lib/GerritUrlReader.ts | 6 +- .../urlReader/lib/GiteaUrlReader.ts | 6 +- .../urlReader/lib/GithubUrlReader.ts | 10 +-- .../urlReader/lib/GitlabUrlReader.ts | 10 +-- .../urlReader/lib/GoogleGcsUrlReader.ts | 6 +- .../urlReader/lib/HarnessUrlReader.ts | 6 +- packages/cli-common/src/run.ts | 10 +-- .../src/lib/versioning/yarn.ts | 3 +- .../lib/execution/executePortableTemplate.ts | 10 +-- packages/cli-node/src/git/GitUtils.ts | 13 ++- packages/cli-node/src/pacman/yarn/Yarn.ts | 7 +- packages/cli/src/wiring/lazy.ts | 5 +- packages/config-loader/src/schema/collect.ts | 8 +- .../src/sources/EnvConfigSource.ts | 5 +- .../src/sources/transform/apply.ts | 5 +- .../LoginRequestListItem.tsx | 4 +- packages/errors/report.api.md | 3 + packages/errors/src/errors/CustomErrorBase.ts | 4 +- packages/errors/src/errors/assertion.test.ts | 84 ++++++++++++++++++- packages/errors/src/errors/assertion.ts | 27 ++++++ packages/errors/src/errors/index.ts | 2 +- packages/repo-tools/src/commands/index.ts | 5 +- .../src/identity/FirestoreKeyStore.ts | 10 +-- plugins/auth-backend/src/providers/router.ts | 11 ++- .../ConsentPage/useConsentSession.ts | 8 +- .../src/engine/IncrementalIngestionEngine.ts | 13 +-- .../DefaultCatalogProcessingEngine.ts | 5 +- .../DefaultCatalogProcessingOrchestrator.ts | 6 +- .../processing/ProcessorOutputCollector.ts | 10 ++- .../src/processors/UrlReaderProcessor.ts | 10 +-- .../StepPrepareCreatePullRequest.tsx | 5 +- .../StepReviewLocation/StepReviewLocation.tsx | 8 +- .../UnregisterEntityDialog.tsx | 8 +- .../DeleteEntityConfirmationDialog.tsx | 4 +- .../DeleteEntityDialog.tsx | 5 +- .../src/api/DevToolsBackendApi.ts | 12 +-- .../src/routers/createStreamableRouter.ts | 9 +- .../src/lib/SlackNotificationProcessor.ts | 6 +- .../src/actions/gitHelpers.ts | 8 +- .../src/actions/githubActionsDispatch.ts | 8 +- .../src/actions/githubIssuesCreate.ts | 8 +- .../src/actions/githubIssuesLabel.ts | 7 +- .../src/actions/githubWebhook.ts | 7 +- .../src/actions/helpers.ts | 30 ++++--- .../src/scaffolder/tasks/TaskWorker.ts | 8 +- .../scaffolder-backend/src/service/helpers.ts | 8 +- .../src/indexing/BatchSearchEngineIndexer.ts | 11 +-- .../src/indexing/DecoratorBase.ts | 11 +-- .../src/DocsBuilder/builder.ts | 8 +- .../src/cache/TechDocsCache.ts | 8 +- .../src/service/DocsSynchronizer.ts | 13 ++- .../src/stages/generate/helpers.ts | 13 +-- .../src/stages/generate/mkdocsPatchers.ts | 17 ++-- .../techdocs-node/src/stages/prepare/url.ts | 8 +- .../techdocs-node/src/stages/publish/awsS3.ts | 25 +++--- .../src/stages/publish/azureBlobStorage.ts | 20 +++-- .../src/stages/publish/googleStorage.ts | 10 +-- .../publish/migrations/GoogleMigration.ts | 5 +- .../src/stages/publish/openStackSwift.ts | 28 +++---- 70 files changed, 400 insertions(+), 345 deletions(-) create mode 100644 .changeset/add-to-error-utility.md diff --git a/.changeset/add-to-error-utility.md b/.changeset/add-to-error-utility.md new file mode 100644 index 0000000000..fd7ce6fe27 --- /dev/null +++ b/.changeset/add-to-error-utility.md @@ -0,0 +1,5 @@ +--- +'@backstage/errors': minor +--- + +A new `toError` utility function is now available for converting unknown values to `ErrorLike` objects. If the value is already error-like it is returned as-is. Strings are used directly as the error message, and other values are stringified with a fallback to JSON to avoid unhelpful messages like `[object Object]`. Non-error causes passed to `CustomErrorBase` are now converted and stored using `toError` rather than discarded. diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 8ebab723da..9ea716cc0e 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -35,7 +35,7 @@ import type { } from '../../../backend-plugin-api/src/wiring/types'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import type { InternalServiceFactory } from '../../../backend-plugin-api/src/services/system/types'; -import { ForwardedError, ConflictError, assertError } from '@backstage/errors'; +import { ConflictError, ForwardedError, toError } from '@backstage/errors'; import { DependencyGraph } from '../lib/DependencyGraph'; import { ServiceRegistry } from './ServiceRegistry'; import { createInitializationResultCollector } from './createInitializationResultCollector'; @@ -385,12 +385,8 @@ export class BackendInitializer { await moduleInit.init.func(moduleDeps); resultCollector.onPluginModuleResult(pluginId, moduleId); } catch (error: unknown) { - assertError(error); - resultCollector.onPluginModuleResult( - pluginId, - moduleId, - error, - ); + const err = toError(error); + resultCollector.onPluginModuleResult(pluginId, moduleId, err); } }, ); @@ -414,8 +410,8 @@ export class BackendInitializer { const lifecycleService = await this.#getPluginLifecycleImpl(pluginId); await lifecycleService.startup(); } catch (error: unknown) { - assertError(error); - resultCollector.onPluginResult(pluginId, error); + const err = toError(error); + resultCollector.onPluginResult(pluginId, err); } }), ).catch(error => { @@ -515,19 +511,19 @@ export class BackendInitializer { throw new Error(`Invalid registration type '${(r as any).type}'`); } } catch (error: unknown) { - assertError(error); + const err = toError(error); // Clean up partially registered extension points for (const id of addedExtensionPointIds) { this.#extensionPoints.delete(id); } if ('pluginId' in r && 'moduleId' in r) { - resultCollector.onPluginModuleResult(r.pluginId, r.moduleId, error); + resultCollector.onPluginModuleResult(r.pluginId, r.moduleId, err); } else if ('pluginId' in r) { pluginInits.delete(r.pluginId); moduleInits.delete(r.pluginId); - resultCollector.onPluginResult(r.pluginId, error); + resultCollector.onPluginResult(r.pluginId, err); } else { - throw error; + throw err; } } } diff --git a/packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.ts b/packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.ts index 2bd797c929..9f8ce2c561 100644 --- a/packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.ts +++ b/packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.ts @@ -16,7 +16,7 @@ import { DiscoveryService, LoggerService } from '@backstage/backend-plugin-api'; import { decodeJwt, importJWK, SignJWT, decodeProtectedHeader } from 'jose'; -import { assertError, AuthenticationError } from '@backstage/errors'; +import { AuthenticationError, toError } from '@backstage/errors'; import { jwtVerify } from 'jose'; import { tokenTypes } from '@backstage/plugin-auth-node'; import { JwksClient } from '../JwksClient'; @@ -210,8 +210,10 @@ export class DefaultPluginTokenHandler implements PluginTokenHandler { this.supportedTargetPlugins.add(targetPluginId); return true; } catch (error) { - assertError(error); - this.logger.error('Unexpected failure for target JWKS check', error); + this.logger.error( + 'Unexpected failure for target JWKS check', + toError(error), + ); return false; } finally { this.targetPluginInflightChecks.delete(targetPluginId); diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/applyInternalErrorFilter.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/applyInternalErrorFilter.ts index 1771fabcd9..afb3a68e72 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/applyInternalErrorFilter.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/applyInternalErrorFilter.ts @@ -15,7 +15,7 @@ */ import { LoggerService } from '@backstage/backend-plugin-api'; -import { assertError } from '@backstage/errors'; +import { isError, toError } from '@backstage/errors'; import { randomBytes } from 'node:crypto'; function handleBadError(error: Error, logger: LoggerService) { @@ -37,11 +37,8 @@ export function applyInternalErrorFilter( error: unknown, logger: LoggerService, ): Error { - try { - assertError(error); - } catch (assertionError: unknown) { - assertError(assertionError); - return handleBadError(assertionError, logger); + if (!isError(error)) { + return handleBadError(toError(error), logger); } const constructorName = error.constructor.name; diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsCodeCommitUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsCodeCommitUrlReader.ts index 10b1061c2d..3202953383 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsCodeCommitUrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsCodeCommitUrlReader.ts @@ -32,11 +32,7 @@ import { AwsCodeCommitIntegration, ScmIntegrations, } from '@backstage/integration'; -import { - assertError, - ForwardedError, - NotModifiedError, -} from '@backstage/errors'; +import { toError, ForwardedError, NotModifiedError } from '@backstage/errors'; import { fromTemporaryCredentials } from '@aws-sdk/credential-providers'; import { CodeCommitClient, @@ -418,8 +414,8 @@ export class AwsCodeCommitUrlReader implements UrlReaderService { ], etag: data.etag ?? '', }; - } catch (error) { - assertError(error); + } catch (e) { + const error = toError(e); if (error.name === 'NotFoundError') { return { files: [], diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsS3UrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsS3UrlReader.ts index 0b17ffe326..946182ab2e 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsS3UrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsS3UrlReader.ts @@ -33,11 +33,7 @@ import { ScmIntegrations, AwsS3IntegrationConfig, } from '@backstage/integration'; -import { - assertError, - ForwardedError, - NotModifiedError, -} from '@backstage/errors'; +import { toError, ForwardedError, NotModifiedError } from '@backstage/errors'; import { fromTemporaryCredentials } from '@aws-sdk/credential-providers'; import { AwsCredentialIdentityProvider } from '@aws-sdk/types'; import { @@ -400,8 +396,8 @@ export class AwsS3UrlReader implements UrlReaderService { ], etag: data.etag ?? '', }; - } catch (error) { - assertError(error); + } catch (e) { + const error = toError(e); if (error.name === 'NotFoundError') { return { files: [], diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureBlobStorageUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureBlobStorageUrlReader.ts index b19295ef99..ec04fd2fd8 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureBlobStorageUrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureBlobStorageUrlReader.ts @@ -21,11 +21,7 @@ import { StorageSharedKeyCredential, } from '@azure/storage-blob'; import { ReaderFactory, ReadTreeResponseFactory } from './types'; -import { - assertError, - ForwardedError, - NotModifiedError, -} from '@backstage/errors'; +import { toError, ForwardedError, NotModifiedError } from '@backstage/errors'; import { Readable } from 'node:stream'; import { relative } from 'node:path/posix'; import { ReadUrlResponseFactory } from './ReadUrlResponseFactory'; @@ -261,9 +257,8 @@ export class AzureBlobStorageUrlReader implements UrlReaderService { ], etag: data.etag ?? '', }; - } catch (error) { - assertError(error); - throw error; + } catch (e) { + throw toError(e); } } diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureUrlReader.ts index 66fd1cc95a..91bf000272 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureUrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureUrlReader.ts @@ -34,11 +34,7 @@ import { } from '@backstage/integration'; import parseGitUrl from 'git-url-parse'; import { Minimatch } from 'minimatch'; -import { - assertError, - NotFoundError, - NotModifiedError, -} from '@backstage/errors'; +import { toError, NotFoundError, NotModifiedError } from '@backstage/errors'; import { ReadTreeResponseFactory, ReaderFactory } from './types'; import { ReadUrlResponseFactory } from './ReadUrlResponseFactory'; @@ -212,8 +208,8 @@ export class AzureUrlReader implements UrlReaderService { ], etag: data.etag ?? '', }; - } catch (error) { - assertError(error); + } catch (e) { + const error = toError(e); if (error.name === 'NotFoundError') { return { files: [], diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.ts index 73302d37e5..b4555ed093 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.ts @@ -23,11 +23,7 @@ import { UrlReaderServiceSearchOptions, UrlReaderServiceSearchResponse, } from '@backstage/backend-plugin-api'; -import { - assertError, - NotFoundError, - NotModifiedError, -} from '@backstage/errors'; +import { toError, NotFoundError, NotModifiedError } from '@backstage/errors'; import { BitbucketCloudIntegration, getBitbucketCloudDefaultBranch, @@ -196,8 +192,8 @@ export class BitbucketCloudUrlReader implements UrlReaderService { ], etag: data.etag ?? '', }; - } catch (error) { - assertError(error); + } catch (e) { + const error = toError(e); if (error.name === 'NotFoundError') { return { files: [], diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketServerUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketServerUrlReader.ts index aba28b4fd7..ed25272dc8 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketServerUrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketServerUrlReader.ts @@ -23,11 +23,7 @@ import { UrlReaderServiceSearchOptions, UrlReaderServiceSearchResponse, } from '@backstage/backend-plugin-api'; -import { - assertError, - NotFoundError, - NotModifiedError, -} from '@backstage/errors'; +import { toError, NotFoundError, NotModifiedError } from '@backstage/errors'; import { BitbucketServerIntegration, getBitbucketServerDownloadUrl, @@ -180,8 +176,8 @@ export class BitbucketServerUrlReader implements UrlReaderService { ], etag: data.etag ?? '', }; - } catch (error) { - assertError(error); + } catch (e) { + const error = toError(e); if (error.name === 'NotFoundError') { return { files: [], diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.ts index f49aae9abb..33920d2112 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.ts @@ -22,11 +22,7 @@ import { UrlReaderServiceSearchOptions, UrlReaderServiceSearchResponse, } from '@backstage/backend-plugin-api'; -import { - assertError, - NotFoundError, - NotModifiedError, -} from '@backstage/errors'; +import { toError, NotFoundError, NotModifiedError } from '@backstage/errors'; import { ReaderFactory } from './types'; import path from 'node:path'; import { ReadUrlResponseFactory } from './ReadUrlResponseFactory'; @@ -236,8 +232,8 @@ export class FetchUrlReader implements UrlReaderService { ], etag: data.etag ?? '', }; - } catch (error) { - assertError(error); + } catch (e) { + const error = toError(e); if (error.name === 'NotFoundError') { return { files: [], diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.ts index 02c36c323b..8cf41f96ff 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.ts @@ -40,7 +40,7 @@ import { NotFoundError, NotModifiedError, ResponseError, - assertError, + toError, } from '@backstage/errors'; import { ReadTreeResponseFactory, ReaderFactory } from './types'; import { Minimatch } from 'minimatch'; @@ -174,8 +174,8 @@ export class GerritUrlReader implements UrlReaderService { ], etag: data.etag ?? '', }; - } catch (error) { - assertError(error); + } catch (e) { + const error = toError(e); if (error.name === 'NotFoundError') { return { files: [], diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GiteaUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GiteaUrlReader.ts index a7579dca38..ab3bef4e8d 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GiteaUrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GiteaUrlReader.ts @@ -35,7 +35,7 @@ import { import { ReaderFactory, ReadTreeResponseFactory } from './types'; import { ReadUrlResponseFactory } from './ReadUrlResponseFactory'; import { - assertError, + toError, AuthenticationError, NotFoundError, NotModifiedError, @@ -189,8 +189,8 @@ export class GiteaUrlReader implements UrlReaderService { ], etag: data.etag ?? '', }; - } catch (error) { - assertError(error); + } catch (e) { + const error = toError(e); if (error.name === 'NotFoundError') { return { files: [], diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GithubUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GithubUrlReader.ts index e5aa08275e..442899d50e 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GithubUrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GithubUrlReader.ts @@ -37,11 +37,7 @@ import fetch, { RequestInit, Response } from 'node-fetch'; import parseGitUrl from 'git-url-parse'; import { Minimatch } from 'minimatch'; import { Readable } from 'node:stream'; -import { - assertError, - NotFoundError, - NotModifiedError, -} from '@backstage/errors'; +import { toError, NotFoundError, NotModifiedError } from '@backstage/errors'; import { ReadTreeResponseFactory, ReaderFactory } from './types'; import { ReadUrlResponseFactory } from './ReadUrlResponseFactory'; import { parseLastModified } from './util'; @@ -209,8 +205,8 @@ export class GithubUrlReader implements UrlReaderService { ], etag: data.etag ?? '', }; - } catch (error) { - assertError(error); + } catch (e) { + const error = toError(e); if (error.name === 'NotFoundError') { return { files: [], diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts index a85b9fbb89..23a39003b6 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts @@ -23,11 +23,7 @@ import { UrlReaderServiceSearchOptions, UrlReaderServiceSearchResponse, } from '@backstage/backend-plugin-api'; -import { - assertError, - NotFoundError, - NotModifiedError, -} from '@backstage/errors'; +import { toError, NotFoundError, NotModifiedError } from '@backstage/errors'; import { getGitLabFileFetchUrl, getGitLabIntegrationRelativePath, @@ -260,8 +256,8 @@ export class GitlabUrlReader implements UrlReaderService { ], etag: data.etag ?? '', }; - } catch (error) { - assertError(error); + } catch (e) { + const error = toError(e); if (error.name === 'NotFoundError') { return { files: [], diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GoogleGcsUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GoogleGcsUrlReader.ts index 8657125671..618f075089 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GoogleGcsUrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GoogleGcsUrlReader.ts @@ -34,7 +34,7 @@ import { import { Readable } from 'node:stream'; import { ReadUrlResponseFactory } from './ReadUrlResponseFactory'; import packageinfo from '../../../../package.json'; -import { assertError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; import { relative } from 'node:path/posix'; const GOOGLE_GCS_HOST = 'storage.cloud.google.com'; @@ -182,8 +182,8 @@ export class GoogleGcsUrlReader implements UrlReaderService { ], etag: data.etag ?? '', }; - } catch (error) { - assertError(error); + } catch (e) { + const error = toError(e); if (error.name === 'NotFoundError') { return { files: [], diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/HarnessUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/HarnessUrlReader.ts index 478bfa14bc..277fe42def 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/HarnessUrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/HarnessUrlReader.ts @@ -36,7 +36,7 @@ import { ReadTreeResponseFactory, ReaderFactory } from './types'; import fetch, { Response } from 'node-fetch'; import { ReadUrlResponseFactory } from './ReadUrlResponseFactory'; import { - assertError, + toError, AuthenticationError, NotFoundError, NotModifiedError, @@ -187,8 +187,8 @@ export class HarnessUrlReader implements UrlReaderService { ], etag: data.etag ?? '', }; - } catch (error) { - assertError(error); + } catch (e) { + const error = toError(e); if (error.name === 'NotFoundError') { return { files: [], diff --git a/packages/cli-common/src/run.ts b/packages/cli-common/src/run.ts index 0e95bd517b..6d35d69c55 100644 --- a/packages/cli-common/src/run.ts +++ b/packages/cli-common/src/run.ts @@ -17,7 +17,7 @@ import { ChildProcess, SpawnOptions } from 'node:child_process'; import spawn from 'cross-spawn'; import { ExitCodeError } from './errors'; -import { assertError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; /** * Callback function that can be used to receive stdout or stderr data from a child process. @@ -193,14 +193,14 @@ export async function runOutput( return Buffer.concat(stdoutChunks).toString().trim(); } catch (error) { - assertError(error); + const err = toError(error); - (error as Error & { stdout?: string }).stdout = + (err as Error & { stdout?: string }).stdout = Buffer.concat(stdoutChunks).toString(); - (error as Error & { stderr?: string }).stderr = + (err as Error & { stderr?: string }).stderr = Buffer.concat(stderrChunks).toString(); - throw error; + throw err; } } diff --git a/packages/cli-module-migrate/src/lib/versioning/yarn.ts b/packages/cli-module-migrate/src/lib/versioning/yarn.ts index 908ddca949..269380abb9 100644 --- a/packages/cli-module-migrate/src/lib/versioning/yarn.ts +++ b/packages/cli-module-migrate/src/lib/versioning/yarn.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { assertError, ForwardedError } from '@backstage/errors'; +import { ForwardedError } from '@backstage/errors'; import { runOutput } from '@backstage/cli-common'; const versions = new Map>(); @@ -32,7 +32,6 @@ export function detectYarnVersion(dir?: string): Promise<'classic' | 'berry'> { }); return stdout.trim().startsWith('1.') ? 'classic' : 'berry'; } catch (error) { - assertError(error); throw new ForwardedError('Failed to determine yarn version', error); } }); diff --git a/packages/cli-module-new/src/lib/execution/executePortableTemplate.ts b/packages/cli-module-new/src/lib/execution/executePortableTemplate.ts index 8e31f740cd..6381dcfcee 100644 --- a/packages/cli-module-new/src/lib/execution/executePortableTemplate.ts +++ b/packages/cli-module-new/src/lib/execution/executePortableTemplate.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { assertError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; import { addCodeownersEntry } from '../codeowners'; import { Task } from '../tasks'; import { @@ -68,9 +68,9 @@ export async function executePortableTemplate( }).waitForExit(); }); } catch (error) { - assertError(error); + const err = toError(error); Task.error( - `Warning: Failed to execute command '${commandStr}', ${error}`, + `Warning: Failed to execute command '${commandStr}', ${err}`, ); } } @@ -80,8 +80,8 @@ export async function executePortableTemplate( Task.log(`🎉 Successfully created ${template.name}`); Task.log(); } catch (error) { - assertError(error); - Task.error(error.message); + const err = toError(error); + Task.error(err.message); if (modified) { Task.log('It seems that something went wrong in the creation process 🤔'); diff --git a/packages/cli-node/src/git/GitUtils.ts b/packages/cli-node/src/git/GitUtils.ts index f2350be309..40fa624498 100644 --- a/packages/cli-node/src/git/GitUtils.ts +++ b/packages/cli-node/src/git/GitUtils.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { assertError, ForwardedError } from '@backstage/errors'; +import { ForwardedError, toError } from '@backstage/errors'; import { targetPaths } from '@backstage/cli-common'; import { runOutput } from '@backstage/cli-common'; @@ -28,13 +28,10 @@ export async function runGit(...args: string[]) { }); return stdout.trim().split(/\r\n|\r|\n/); } catch (error) { - assertError(error); - if ( - 'code' in error && - typeof (error as { code?: number }).code === 'number' - ) { - const code = (error as { code?: number }).code; - const stderr = (error as { stderr?: string }).stderr; + const err = toError(error); + if ('code' in err && typeof (err as { code?: number }).code === 'number') { + const code = (err as { code?: number }).code; + const stderr = (err as { stderr?: string }).stderr; const msg = stderr?.trim() ?? `with exit code ${code}`; throw new Error(`git ${args[0]} failed, ${msg}`); } diff --git a/packages/cli-node/src/pacman/yarn/Yarn.ts b/packages/cli-node/src/pacman/yarn/Yarn.ts index 7aee7bdebf..2fb8596351 100644 --- a/packages/cli-node/src/pacman/yarn/Yarn.ts +++ b/packages/cli-node/src/pacman/yarn/Yarn.ts @@ -14,11 +14,7 @@ * limitations under the License. */ -import { - assertError, - ForwardedError, - NotImplementedError, -} from '@backstage/errors'; +import { ForwardedError, NotImplementedError } from '@backstage/errors'; import { PackageInfo, PackageManager } from '../PackageManager'; import { Lockfile } from '../Lockfile'; import { YarnVersion } from './types'; @@ -96,7 +92,6 @@ function detectYarnVersion(dir?: string): Promise { : 'berry'; return { version: versionString, codename }; } catch (error) { - assertError(error); throw new ForwardedError('Failed to determine yarn version', error); } }); diff --git a/packages/cli/src/wiring/lazy.ts b/packages/cli/src/wiring/lazy.ts index 64a4d43ef4..df865a2c18 100644 --- a/packages/cli/src/wiring/lazy.ts +++ b/packages/cli/src/wiring/lazy.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { assertError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; import { exitWithError } from './errors'; type ActionFunc = (...args: any[]) => Promise; @@ -40,8 +40,7 @@ export function lazy( process.exit(0); } catch (error) { - assertError(error); - exitWithError(error); + exitWithError(toError(error)); } }; } diff --git a/packages/config-loader/src/schema/collect.ts b/packages/config-loader/src/schema/collect.ts index e3e416bbfd..232d96c295 100644 --- a/packages/config-loader/src/schema/collect.ts +++ b/packages/config-loader/src/schema/collect.ts @@ -24,7 +24,7 @@ import { } from 'node:path'; import { ConfigSchemaPackageEntry } from './types'; import { JsonObject } from '@backstage/types'; -import { assertError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; type Item = { name?: string; @@ -249,9 +249,9 @@ async function compileTsSchemas( ); } } catch (error) { - assertError(error); - if (error.message !== 'type Config not found') { - throw error; + const err = toError(error); + if (err.message !== 'type Config not found') { + throw err; } } diff --git a/packages/config-loader/src/sources/EnvConfigSource.ts b/packages/config-loader/src/sources/EnvConfigSource.ts index 988110b370..775fd32025 100644 --- a/packages/config-loader/src/sources/EnvConfigSource.ts +++ b/packages/config-loader/src/sources/EnvConfigSource.ts @@ -15,7 +15,7 @@ */ import { AppConfig } from '@backstage/config'; -import { assertError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; import { JsonObject } from '@backstage/types'; import { AsyncConfigSourceGenerator, ConfigSource } from './types'; @@ -165,7 +165,6 @@ function safeJsonParse(str: string): [Error | null, any] { try { return [null, JSON.parse(str)]; } catch (err) { - assertError(err); - return [err, str]; + return [toError(err), str]; } } diff --git a/packages/config-loader/src/sources/transform/apply.ts b/packages/config-loader/src/sources/transform/apply.ts index 89ca647a1a..2ba86e7ff1 100644 --- a/packages/config-loader/src/sources/transform/apply.ts +++ b/packages/config-loader/src/sources/transform/apply.ts @@ -15,7 +15,7 @@ */ import { JsonObject, JsonValue } from '@backstage/types'; -import { assertError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; import { TransformContext, TransformFunc } from './types'; import { isObject } from './utils'; import { createSubstitutionTransform } from './substitution'; @@ -50,8 +50,7 @@ export async function applyConfigTransforms( break; } } catch (error) { - assertError(error); - throw new Error(`error at ${path}, ${error.message}`); + throw new Error(`error at ${path}, ${toError(error).message}`); } } diff --git a/packages/core-components/src/components/OAuthRequestDialog/LoginRequestListItem.tsx b/packages/core-components/src/components/OAuthRequestDialog/LoginRequestListItem.tsx index 0067f2d82c..beea766f4e 100644 --- a/packages/core-components/src/components/OAuthRequestDialog/LoginRequestListItem.tsx +++ b/packages/core-components/src/components/OAuthRequestDialog/LoginRequestListItem.tsx @@ -21,7 +21,7 @@ import ListItemText from '@material-ui/core/ListItemText'; import Typography from '@material-ui/core/Typography'; import Button from '@material-ui/core/Button'; import { createElement, isValidElement, useState } from 'react'; -import { isError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; import { configApiRef, IconComponent, @@ -63,7 +63,7 @@ const LoginRequestListItem = ({ request, busy, setBusy }: RowProps) => { try { await request.trigger(); } catch (e) { - setError(isError(e) ? e.message : 'An unspecified error occurred'); + setError(toError(e).message); } finally { setBusy(false); } diff --git a/packages/errors/report.api.md b/packages/errors/report.api.md index 894aac8413..4bb50cc5ea 100644 --- a/packages/errors/report.api.md +++ b/packages/errors/report.api.md @@ -155,4 +155,7 @@ export class ServiceUnavailableError extends CustomErrorBase {} // @public export function stringifyError(error: unknown): string; + +// @public +export function toError(value: unknown): ErrorLike; ``` diff --git a/packages/errors/src/errors/CustomErrorBase.ts b/packages/errors/src/errors/CustomErrorBase.ts index ea9c91f173..8b08887aa1 100644 --- a/packages/errors/src/errors/CustomErrorBase.ts +++ b/packages/errors/src/errors/CustomErrorBase.ts @@ -15,7 +15,7 @@ */ import { stringifyError } from '../serialization/error'; -import { isError } from './assertion'; +import { toError } from './assertion'; /** * A base class that custom Error classes can inherit from. @@ -63,6 +63,6 @@ export class CustomErrorBase extends Error { } } - this.cause = isError(cause) ? cause : undefined; + this.cause = cause !== undefined ? toError(cause) : undefined; } } diff --git a/packages/errors/src/errors/assertion.test.ts b/packages/errors/src/errors/assertion.test.ts index 28367c2a68..e586c39e54 100644 --- a/packages/errors/src/errors/assertion.test.ts +++ b/packages/errors/src/errors/assertion.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { assertError, isError } from './assertion'; +import { assertError, isError, toError } from './assertion'; import { NotFoundError } from './common'; import { CustomErrorBase } from './CustomErrorBase'; @@ -77,3 +77,85 @@ describe('isError', () => { }, ); }); + +describe('toError', () => { + it.each(areErrors)( + 'should pass through error-like values as-is %#', + error => { + expect(toError(error)).toBe(error); + }, + ); + + it('should preserve the original error instance', () => { + const original = new NotFoundError('not found'); + expect(toError(original)).toBe(original); + }); + + it('should use strings directly as the error message', () => { + const result = toError('something went wrong'); + expect(result).toBeInstanceOf(Error); + expect(result.message).toBe('something went wrong'); + }); + + it('should handle empty strings', () => { + const result = toError(''); + expect(result).toBeInstanceOf(Error); + expect(result.message).toBe(''); + }); + + it('should wrap undefined', () => { + const result = toError(undefined); + expect(result).toBeInstanceOf(Error); + expect(result.message).toBe("unknown error 'undefined'"); + }); + + it('should wrap null', () => { + const result = toError(null); + expect(result).toBeInstanceOf(Error); + expect(result.message).toBe("unknown error 'null'"); + }); + + it('should wrap numbers', () => { + expect(toError(0).message).toBe("unknown error '0'"); + expect(toError(42).message).toBe("unknown error '42'"); + }); + + it('should wrap booleans', () => { + expect(toError(false).message).toBe("unknown error 'false'"); + expect(toError(true).message).toBe("unknown error 'true'"); + }); + + it('should wrap plain objects using JSON when toString is unhelpful', () => { + expect(toError({ name: 'e' }).message).toBe(`unknown error '{"name":"e"}'`); + expect(toError({ message: '' }).message).toBe( + `unknown error '{"message":""}'`, + ); + expect(toError({ code: 404, detail: 'missing' }).message).toBe( + `unknown error '{"code":404,"detail":"missing"}'`, + ); + }); + + it('should fall back to [object Object] for empty plain objects', () => { + expect(toError({}).message).toBe("unknown error '[object Object]'"); + }); + + it('should wrap arrays', () => { + expect(toError([]).message).toBe("unknown error ''"); + expect(toError([1, 2]).message).toBe("unknown error '1,2'"); + }); + + it('should handle objects with a custom toString', () => { + const obj = { toString: () => 'custom string' }; + expect(toError(obj).message).toBe("unknown error 'custom string'"); + }); + + it('should handle symbols', () => { + const result = toError(Symbol('test')); + expect(result).toBeInstanceOf(Error); + expect(result.message).toBe("unknown error 'Symbol(test)'"); + }); + + it('should handle BigInt', () => { + expect(toError(BigInt(42)).message).toBe("unknown error '42'"); + }); +}); diff --git a/packages/errors/src/errors/assertion.ts b/packages/errors/src/errors/assertion.ts index 7ba0ae6984..90337bfd53 100644 --- a/packages/errors/src/errors/assertion.ts +++ b/packages/errors/src/errors/assertion.ts @@ -71,3 +71,30 @@ export function assertError(value: unknown): asserts value is ErrorLike { ); } } + +/** + * Converts an unknown value to an {@link ErrorLike} object. + * + * If the value is already an {@link ErrorLike} object, it is returned as-is. Otherwise, a new + * `Error` is created with the value stringified as the message. + * + * @public + * @param value - an unknown value + * @returns an {@link ErrorLike} object + */ +export function toError(value: unknown): ErrorLike { + if (isError(value)) { + return value; + } + if (typeof value === 'string') { + return new Error(value) as ErrorLike; + } + const str = String(value); + if (str === '[object Object]') { + const json = JSON.stringify(value); + if (json !== '{}') { + return new Error(`unknown error '${json}'`) as ErrorLike; + } + } + return new Error(`unknown error '${str}'`) as ErrorLike; +} diff --git a/packages/errors/src/errors/index.ts b/packages/errors/src/errors/index.ts index 0a59891f87..0c74c4ab4f 100644 --- a/packages/errors/src/errors/index.ts +++ b/packages/errors/src/errors/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export { assertError, isError } from './assertion'; +export { assertError, isError, toError } from './assertion'; export type { ErrorLike } from './assertion'; export { AuthenticationError, diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts index 371d0609ff..51dfdaf668 100644 --- a/packages/repo-tools/src/commands/index.ts +++ b/packages/repo-tools/src/commands/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { assertError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; import { Command } from 'commander'; import { exitWithError } from '../lib/errors'; @@ -302,8 +302,7 @@ export function lazy( process.exit(0); } catch (error) { - assertError(error); - exitWithError(error); + exitWithError(toError(error)); } }; } diff --git a/plugins/auth-backend/src/identity/FirestoreKeyStore.ts b/plugins/auth-backend/src/identity/FirestoreKeyStore.ts index 437b605f59..cc0c7754e3 100644 --- a/plugins/auth-backend/src/identity/FirestoreKeyStore.ts +++ b/plugins/auth-backend/src/identity/FirestoreKeyStore.ts @@ -23,6 +23,7 @@ import { WriteResult, } from '@google-cloud/firestore'; +import { toError } from '@backstage/errors'; import { AnyJWK, KeyStore, StoredKey } from './types'; export type FirestoreKeyStoreSettings = Settings & Options; @@ -66,14 +67,11 @@ export class FirestoreKeyStore implements KeyStore { try { await keyStore.verify(); } catch (error) { + const err = toError(error); if (process.env.NODE_ENV !== 'development') { - throw new Error( - `Failed to connect to database: ${(error as Error).message}`, - ); + throw new Error(`Failed to connect to database: ${err.message}`); } - logger?.warn( - `Failed to connect to database: ${(error as Error).message}`, - ); + logger?.warn(`Failed to connect to database: ${err.message}`); } } diff --git a/plugins/auth-backend/src/providers/router.ts b/plugins/auth-backend/src/providers/router.ts index 8505c1f2bd..8c3df262cc 100644 --- a/plugins/auth-backend/src/providers/router.ts +++ b/plugins/auth-backend/src/providers/router.ts @@ -16,7 +16,7 @@ import { AuthService, LoggerService } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; -import { assertError, NotFoundError } from '@backstage/errors'; +import { NotFoundError, toError } from '@backstage/errors'; import { AuthOwnershipResolver, AuthProviderFactory, @@ -104,14 +104,17 @@ export function bindProviderRouters( targetRouter.use(`/${providerId}`, r); } catch (e) { - assertError(e); if (process.env.NODE_ENV !== 'development') { throw new Error( - `Failed to initialize ${providerId} auth provider, ${e.message}`, + `Failed to initialize ${providerId} auth provider, ${ + toError(e).message + }`, ); } - logger.warn(`Skipping ${providerId} auth provider, ${e.message}`); + logger.warn( + `Skipping ${providerId} auth provider, ${toError(e).message}`, + ); targetRouter.use(`/${providerId}`, () => { // If the user added the provider under auth.providers but the clientId and clientSecret etc. were not found. diff --git a/plugins/auth/src/components/ConsentPage/useConsentSession.ts b/plugins/auth/src/components/ConsentPage/useConsentSession.ts index a19faae6fe..d416c66a0b 100644 --- a/plugins/auth/src/components/ConsentPage/useConsentSession.ts +++ b/plugins/auth/src/components/ConsentPage/useConsentSession.ts @@ -23,7 +23,7 @@ import { import { useCallback } from 'react'; import useAsync from 'react-use/esm/useAsync'; import useAsyncFn from 'react-use/esm/useAsyncFn'; -import { isError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; interface Session { id: string; @@ -111,9 +111,7 @@ export const useConsentSession = (opts: { sessionId?: string }) => { if (sessionState.error) { return { status: 'error', - error: isError(sessionState.error) - ? sessionState.error.message - : 'Failed to load consent request', + error: toError(sessionState.error).message, }; } if (sessionState.value) { @@ -133,7 +131,7 @@ export const useConsentSession = (opts: { sessionId?: string }) => { await handleActionInternal(action, state.session); } catch (err) { alertApi.post({ - message: isError(err) ? err.message : `Failed to ${action} consent`, + message: toError(err).message, severity: 'error', }); } diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/engine/IncrementalIngestionEngine.ts b/plugins/catalog-backend-module-incremental-ingestion/src/engine/IncrementalIngestionEngine.ts index 2f5aede6f8..2cfa5904f4 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/engine/IncrementalIngestionEngine.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/engine/IncrementalIngestionEngine.ts @@ -21,7 +21,7 @@ import { IncrementalIngestionDatabaseManager } from '../database/IncrementalInge import { performance } from 'node:perf_hooks'; import { Duration } from 'luxon'; import { v4 } from 'uuid'; -import { stringifyError } from '@backstage/errors'; +import { stringifyError, toError } from '@backstage/errors'; import { EventParams } from '@backstage/plugin-events-node'; import { HumanDuration } from '@backstage/types'; @@ -123,17 +123,12 @@ export class IncrementalIngestionEngine implements IterationEngine { ); } } catch (error) { - if ( - (error as Error).message && - (error as Error).message === 'CANCEL' - ) { + const err = toError(error); + if (err.message === 'CANCEL') { this.options.logger.info( `incremental-engine: Ingestion '${ingestionId}' canceled`, ); - await this.manager.setProviderCanceling( - ingestionId, - (error as Error).message, - ); + await this.manager.setProviderCanceling(ingestionId, err.message); } else { const currentBackoff = Duration.fromObject( this.backoff[Math.min(this.backoff.length - 1, attempts)], diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts index dc97868b1a..e7865cd8d2 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts @@ -19,7 +19,7 @@ import { Entity, stringifyEntityRef, } from '@backstage/catalog-model'; -import { assertError, serializeError, stringifyError } from '@backstage/errors'; +import { serializeError, stringifyError, toError } from '@backstage/errors'; import { Hash } from 'node:crypto'; import stableStringify from 'fast-json-stable-stringify'; import { Knex } from 'knex'; @@ -344,8 +344,7 @@ export class DefaultCatalogProcessingEngine { track.markSuccessfulWithChanges(); } catch (error) { - assertError(error); - track.markFailed(error); + track.markFailed(toError(error)); } }); }, diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts index e97a8c3843..ee33e62187 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts @@ -24,10 +24,10 @@ import { stringifyLocationRef, } from '@backstage/catalog-model'; import { - assertError, ConflictError, InputError, NotAllowedError, + toError, } from '@backstage/errors'; import { JsonValue } from '@backstage/types'; import { ScmIntegrationRegistry } from '@backstage/integration'; @@ -192,10 +192,10 @@ export class DefaultCatalogProcessingOrchestrator ok: collectorResults.errors.length === 0, }; } catch (error) { - assertError(error); + const err = toError(error); return { ok: false, - errors: collector.results().errors.concat(error), + errors: collector.results().errors.concat(err), }; } } diff --git a/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts b/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts index 03b8415d56..5051ab320f 100644 --- a/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts +++ b/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts @@ -21,7 +21,7 @@ import { stringifyEntityRef, stringifyLocationRef, } from '@backstage/catalog-model'; -import { assertError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; import { CatalogProcessor, CatalogProcessorResult, @@ -97,9 +97,11 @@ export class ProcessorOutputCollector { try { entity = validateEntityEnvelope(i.entity); } catch (e) { - assertError(e); - logger.debug(`Envelope validation failed at ${location}, ${e}`); - this.errors.push(e); + const validationError = toError(e); + logger.debug( + `Envelope validation failed at ${location}, ${validationError}`, + ); + this.errors.push(validationError); return; } diff --git a/plugins/catalog-backend/src/processors/UrlReaderProcessor.ts b/plugins/catalog-backend/src/processors/UrlReaderProcessor.ts index 806c5b9f3d..24ddf6e072 100644 --- a/plugins/catalog-backend/src/processors/UrlReaderProcessor.ts +++ b/plugins/catalog-backend/src/processors/UrlReaderProcessor.ts @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; -import { assertError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; import limiterFactory, { Limit } from 'p-limit'; import { LocationSpec } from '@backstage/plugin-catalog-common'; import { @@ -109,18 +109,18 @@ export class UrlReaderProcessor implements CatalogProcessor { emit(processingResult.refresh(`${location.type}:${location.target}`)); } catch (error) { - assertError(error); - const message = `Unable to read ${location.type}, ${error}`.substring( + const err = toError(error); + const message = `Unable to read ${location.type}, ${err}`.substring( 0, 5000, ); - if (error.name === 'NotModifiedError' && cacheItem) { + if (err.name === 'NotModifiedError' && cacheItem) { for (const parseResult of cacheItem.value) { emit(parseResult); } emit(processingResult.refresh(`${location.type}:${location.target}`)); await cache.set(CACHE_KEY, cacheItem); - } else if (error.name === 'NotFoundError') { + } else if (err.name === 'NotFoundError') { if (!optional) { emit(processingResult.notFoundError(location, message)); } diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx index e42de5257e..40b639f14a 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx @@ -16,7 +16,7 @@ import { Entity } from '@backstage/catalog-model'; import { errorApiRef, useApi } from '@backstage/core-plugin-api'; -import { assertError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; import { catalogApiRef, @@ -210,8 +210,7 @@ export const StepPrepareCreatePullRequest = ( { notRepeatable: true }, ); } catch (e) { - assertError(e); - setError(e.message); + setError(toError(e).message); setSubmitted(false); } }, diff --git a/plugins/catalog-import/src/components/StepReviewLocation/StepReviewLocation.tsx b/plugins/catalog-import/src/components/StepReviewLocation/StepReviewLocation.tsx index ddf2cd51d5..ee6ad01500 100644 --- a/plugins/catalog-import/src/components/StepReviewLocation/StepReviewLocation.tsx +++ b/plugins/catalog-import/src/components/StepReviewLocation/StepReviewLocation.tsx @@ -17,7 +17,7 @@ import { stringifyEntityRef } from '@backstage/catalog-model'; import { Link } from '@backstage/core-components'; import { configApiRef, useAnalytics, useApi } from '@backstage/core-plugin-api'; -import { assertError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import FormHelperText from '@material-ui/core/FormHelperText'; @@ -94,12 +94,12 @@ export const StepReviewLocation = ({ locations, }); } catch (e) { - assertError(e); + const caughtError = toError(e); // TODO: this error should be handled differently. We add it as 'optional' and // it is not uncommon that a PR has not been merged yet. if ( prepareResult.type === 'repository' && - e.message.startsWith( + caughtError.message.startsWith( 'Location was added but has no entities specified yet', ) ) { @@ -111,7 +111,7 @@ export const StepReviewLocation = ({ })), }); } else { - setError(e.message); + setError(caughtError.message); setSubmitted(false); } } diff --git a/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx index d1e13f4ef1..aed0fd2072 100644 --- a/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx @@ -25,7 +25,7 @@ import { import { alertApiRef, configApiRef, useApi } from '@backstage/core-plugin-api'; import { Progress, ResponseErrorPanel } from '@backstage/core-components'; -import { assertError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; import { catalogReactTranslationRef } from '../../translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import { @@ -75,8 +75,7 @@ function useUnregisterDialogHandlers( await state.unregisterLocation(); onConfirm(); } catch (err) { - assertError(err); - alertApi.post({ message: err.message }); + alertApi.post({ message: toError(err).message }); } finally { setBusyAction(null); } @@ -98,8 +97,7 @@ function useUnregisterDialogHandlers( display: 'transient', }); } catch (err) { - assertError(err); - alertApi.post({ message: err.message }); + alertApi.post({ message: toError(err).message }); } finally { setBusyAction(null); } diff --git a/plugins/catalog-unprocessed-entities/src/components/DeleteEntityConfirmationDialog.tsx b/plugins/catalog-unprocessed-entities/src/components/DeleteEntityConfirmationDialog.tsx index 91a67551c9..641f95c4b2 100644 --- a/plugins/catalog-unprocessed-entities/src/components/DeleteEntityConfirmationDialog.tsx +++ b/plugins/catalog-unprocessed-entities/src/components/DeleteEntityConfirmationDialog.tsx @@ -19,7 +19,7 @@ import Dialog from '@material-ui/core/Dialog'; import DialogActions from '@material-ui/core/DialogActions'; import DialogTitle from '@material-ui/core/DialogTitle'; import { useState } from 'react'; -import { assertError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; interface DeleteEntityConfirmationProps { open: boolean; @@ -37,7 +37,7 @@ export function DeleteEntityConfirmationDialog( try { onConfirm(); } catch (err) { - assertError(err); + void toError(err); } finally { setBusy(false); } diff --git a/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx index 2c350aebc5..6c3f5b4117 100644 --- a/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx +++ b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx @@ -18,7 +18,7 @@ import { Entity } from '@backstage/catalog-model'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { useState } from 'react'; import { alertApiRef, useApi } from '@backstage/core-plugin-api'; -import { assertError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import { Button, Dialog, DialogFooter, DialogHeader } from '@backstage/ui'; @@ -44,8 +44,7 @@ export function DeleteEntityDialog(props: DeleteEntityDialogProps) { await catalogApi.removeEntityByUid(uid!); onConfirm(); } catch (err) { - assertError(err); - alertApi.post({ message: err.message }); + alertApi.post({ message: toError(err).message }); } finally { setBusy(false); } diff --git a/plugins/devtools-backend/src/api/DevToolsBackendApi.ts b/plugins/devtools-backend/src/api/DevToolsBackendApi.ts index 3afda59c80..e518cf65b1 100644 --- a/plugins/devtools-backend/src/api/DevToolsBackendApi.ts +++ b/plugins/devtools-backend/src/api/DevToolsBackendApi.ts @@ -32,7 +32,7 @@ import os from 'node:os'; import fs from 'fs-extra'; import { Lockfile } from '../util/Lockfile'; import { memoize } from 'lodash'; -import { assertError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; import { LoggerService } from '@backstage/backend-plugin-api'; /** @public */ @@ -179,7 +179,7 @@ export class DevToolsBackendApi { const data = ConfigReader.fromConfigs(sanitizedConfigs).get(); configInfo.config = data; } catch (error) { - assertError(error); + const err = toError(error); // The config is not valid for some reason but we want to be able to see it still const config = { data: this.config.get() as JsonObject, @@ -194,10 +194,10 @@ export class DevToolsBackendApi { const data = ConfigReader.fromConfigs(sanitizedConfigs).get(); configInfo.config = data; configInfo.error = { - name: error.name, - message: error.message, - messages: error.messages as string[] | undefined, - stack: error.stack, + name: err.name, + message: err.message, + messages: err.messages as string[] | undefined, + stack: err.stack, }; } diff --git a/plugins/mcp-actions-backend/src/routers/createStreamableRouter.ts b/plugins/mcp-actions-backend/src/routers/createStreamableRouter.ts index 0225bbb98d..517aa18824 100644 --- a/plugins/mcp-actions-backend/src/routers/createStreamableRouter.ts +++ b/plugins/mcp-actions-backend/src/routers/createStreamableRouter.ts @@ -20,7 +20,7 @@ import { McpService } from '../services/McpService'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js'; import { HttpAuthService, LoggerService } from '@backstage/backend-plugin-api'; -import { isError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; import { MetricsService } from '@backstage/backend-plugin-api/alpha'; import { bucketBoundaries, McpServerSessionAttributes } from '../metrics'; import { McpServerConfig } from '../config'; @@ -83,11 +83,10 @@ export const createStreamableRouter = ({ sessionDuration.record(durationSeconds, baseAttributes); }); } catch (error) { - const errorType = isError(error) ? error.name : 'Error'; + const err = toError(error); + const errorType = err.name; - if (isError(error)) { - logger.error(error.message); - } + logger.error(err.message); if (!res.headersSent) { res.status(500).json({ diff --git a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts index 28846fcbb5..9a95b61210 100644 --- a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts @@ -27,7 +27,7 @@ import { UserEntity, } from '@backstage/catalog-model'; import { Config, readDurationFromConfig } from '@backstage/config'; -import { NotFoundError } from '@backstage/errors'; +import { NotFoundError, toError } from '@backstage/errors'; import { Notification } from '@backstage/plugin-notifications-common'; import { NotificationProcessor, @@ -238,9 +238,7 @@ export class SlackNotificationProcessor implements NotificationProcessor { channel = await this.getSlackNotificationTarget(entityRef); } catch (error) { this.logger.error( - `Failed to get Slack channel for entity: ${ - (error as Error).message - }`, + `Failed to get Slack channel for entity: ${toError(error).message}`, ); return; } diff --git a/plugins/scaffolder-backend-module-github/src/actions/gitHelpers.ts b/plugins/scaffolder-backend-module-github/src/actions/gitHelpers.ts index d7ef3496dd..ad1b188348 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/gitHelpers.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/gitHelpers.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { assertError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; import { Octokit } from 'octokit'; import { LoggerService } from '@backstage/backend-plugin-api'; @@ -109,9 +109,9 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({ }); } } catch (e) { - assertError(e); + const error = toError(e); if ( - e.message.includes( + error.message.includes( 'Upgrade to GitHub Pro or make this repository public to enable this feature', ) ) { @@ -119,7 +119,7 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({ 'Branch protection was not enabled as it requires GitHub Pro for private repositories', ); } else { - throw e; + throw error; } } }; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts index 00dd72a9e3..5fd97c30d0 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { assertError, InputError } from '@backstage/errors'; +import { InputError, toError } from '@backstage/errors'; import { GithubCredentialsProvider, ScmIntegrations, @@ -175,11 +175,11 @@ export function createGithubActionsDispatchAction(options: { } } } catch (e) { - assertError(e); + const error = toError(e); ctx.logger.warn( - `Failed: dispatching workflow '${workflowId}' on repo: '${repo}', ${e.message}`, + `Failed: dispatching workflow '${workflowId}' on repo: '${repo}', ${error.message}`, ); - throw e; + throw error; } }, }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.ts index 5c5703e20d..1dd845277d 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.ts @@ -22,7 +22,7 @@ import { createTemplateAction, parseRepoUrl, } from '@backstage/plugin-scaffolder-node'; -import { assertError, InputError } from '@backstage/errors'; +import { InputError, toError } from '@backstage/errors'; import { Octokit } from 'octokit'; import { getOctokitOptions } from '../util'; import { examples } from './githubIssuesCreate.examples'; @@ -171,11 +171,11 @@ export function createGithubIssuesCreateAction(options: { `Successfully created issue #${issue.number}: ${issue.html_url}`, ); } catch (e) { - assertError(e); + const error = toError(e); ctx.logger.warn( - `Failed: creating issue '${title}' on repo: '${repo}', ${e.message}`, + `Failed: creating issue '${title}' on repo: '${repo}', ${error.message}`, ); - throw e; + throw error; } }, }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.ts index bbda5a1839..52c8eab484 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.ts @@ -22,7 +22,7 @@ import { createTemplateAction, parseRepoUrl, } from '@backstage/plugin-scaffolder-node'; -import { assertError, InputError } from '@backstage/errors'; +import { InputError, toError } from '@backstage/errors'; import { Octokit } from 'octokit'; import { getOctokitOptions } from '../util'; import { examples } from './githubIssuesLabel.examples'; @@ -101,9 +101,10 @@ export function createGithubIssuesLabelAction(options: { }, }); } catch (e) { - assertError(e); ctx.logger.warn( - `Failed: adding labels to issue: '${number}' on repo: '${repo}', ${e.message}`, + `Failed: adding labels to issue: '${number}' on repo: '${repo}', ${ + toError(e).message + }`, ); } }, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.ts b/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.ts index 3faab03f13..61010f8bf8 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.ts @@ -23,7 +23,7 @@ import { parseRepoUrl, } from '@backstage/plugin-scaffolder-node'; import { emitterEventNames } from '@octokit/webhooks'; -import { assertError, InputError } from '@backstage/errors'; +import { InputError, toError } from '@backstage/errors'; import { Octokit } from 'octokit'; import { getOctokitOptions } from '../util'; import { examples } from './githubWebhook.examples'; @@ -173,9 +173,10 @@ export function createGithubWebhookAction(options: { ctx.logger.info(`Webhook '${webhookUrl}' created successfully`); } catch (e) { - assertError(e); ctx.logger.warn( - `Failed: create webhook '${webhookUrl}' on repo: '${repo}', ${e.message}`, + `Failed: create webhook '${webhookUrl}' on repo: '${repo}', ${ + toError(e).message + }`, ); } }, diff --git a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts index 6f73e6a770..d0ef19a1f2 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts @@ -15,7 +15,7 @@ */ import { Config } from '@backstage/config'; -import { assertError, NotFoundError } from '@backstage/errors'; +import { NotFoundError, toError } from '@backstage/errors'; import { Octokit } from 'octokit'; import { @@ -124,14 +124,15 @@ export async function createGithubRepoWithCollaboratorsAndTopics( try { newRepo = (await repoCreationPromise).data; } catch (e) { - assertError(e); - if (e.message === 'Resource not accessible by integration') { + if (toError(e).message === 'Resource not accessible by integration') { logger.warn( `The GitHub app or token provided may not have the required permissions to create the ${user.data.type} repository ${owner}/${repo}.`, ); } throw new Error( - `Failed to create the ${user.data.type} repository ${owner}/${repo}, ${e.message}`, + `Failed to create the ${user.data.type} repository ${owner}/${repo}, ${ + toError(e).message + }`, ); } @@ -174,10 +175,11 @@ export async function createGithubRepoWithCollaboratorsAndTopics( }); } } catch (e) { - assertError(e); const name = extractCollaboratorName(collaborator); logger.warn( - `Skipping ${collaborator.access} access for ${name}, ${e.message}`, + `Skipping ${collaborator.access} access for ${name}, ${ + toError(e).message + }`, ); } } @@ -191,8 +193,7 @@ export async function createGithubRepoWithCollaboratorsAndTopics( names: topics.map(t => t.toLowerCase()), }); } catch (e) { - assertError(e); - logger.warn(`Skipping topics ${topics.join(' ')}, ${e.message}`); + logger.warn(`Skipping topics ${topics.join(' ')}, ${toError(e).message}`); } } @@ -356,9 +357,10 @@ export async function initRepoPushAndProtect( requiredLinearHistory, }); } catch (e) { - assertError(e); logger.warn( - `Skipping: default branch protection on '${repo}', ${e.message}`, + `Skipping: default branch protection on '${repo}', ${ + toError(e).message + }`, ); } } @@ -369,8 +371,12 @@ export async function initRepoPushAndProtect( function extractCollaboratorName( collaborator: { user: string } | { team: string } | { username: string }, ) { - if ('username' in collaborator) return collaborator.username; - if ('user' in collaborator) return collaborator.user; + if ('username' in collaborator) { + return collaborator.username; + } + if ('user' in collaborator) { + return collaborator.user; + } return collaborator.team; } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index fdae61a84b..ca3ef3918e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -16,7 +16,7 @@ import { AuditorService, LoggerService } from '@backstage/backend-plugin-api'; import type { MetricsService } from '@backstage/backend-plugin-api/alpha'; -import { assertError, InputError, stringifyError } from '@backstage/errors'; +import { InputError, stringifyError, toError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { @@ -228,12 +228,12 @@ export class TaskWorker { await task.complete('completed', { output }); await auditorEvent?.success(); } catch (error) { - assertError(error); + const err = toError(error); await auditorEvent?.fail({ - error, + error: err, }); await task.complete('failed', { - error: { name: error.name, message: error.message }, + error: { name: err.name, message: err.message }, }); } } diff --git a/plugins/scaffolder-backend/src/service/helpers.ts b/plugins/scaffolder-backend/src/service/helpers.ts index ae79ef0551..b99129d92c 100644 --- a/plugins/scaffolder-backend/src/service/helpers.ts +++ b/plugins/scaffolder-backend/src/service/helpers.ts @@ -27,7 +27,7 @@ import { stringifyEntityRef, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { assertError, InputError, NotFoundError } from '@backstage/errors'; +import { InputError, NotFoundError, toError } from '@backstage/errors'; import { CatalogService } from '@backstage/plugin-catalog-node'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import fs from 'fs-extra'; @@ -47,13 +47,13 @@ export async function getWorkingDirectory( await fs.access(workingDirectory, fs.constants.F_OK | fs.constants.W_OK); logger.info(`using working directory: ${workingDirectory}`); } catch (err) { - assertError(err); + const error = toError(err); logger.error( `working directory ${workingDirectory} ${ - err.code === 'ENOENT' ? 'does not exist' : 'is not writable' + error.code === 'ENOENT' ? 'does not exist' : 'is not writable' }`, ); - throw err; + throw error; } return workingDirectory; } diff --git a/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts index d1915fbedf..5e4284e205 100644 --- a/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts +++ b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { assertError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; import { IndexableDocument } from '@backstage/plugin-search-common'; import { Writable } from 'node:stream'; @@ -66,8 +66,7 @@ export abstract class BatchSearchEngineIndexer extends Writable { await this.initialize(); done(); } catch (e) { - assertError(e); - done(e); + done(toError(e)); } } @@ -91,8 +90,7 @@ export abstract class BatchSearchEngineIndexer extends Writable { this.currentBatch = []; done(); } catch (e) { - assertError(e); - done(e); + done(toError(e)); } } @@ -110,8 +108,7 @@ export abstract class BatchSearchEngineIndexer extends Writable { await this.finalize(); done(); } catch (e) { - assertError(e); - done(e); + done(toError(e)); } } } diff --git a/plugins/search-backend-node/src/indexing/DecoratorBase.ts b/plugins/search-backend-node/src/indexing/DecoratorBase.ts index f2eede461e..75e99bb23c 100644 --- a/plugins/search-backend-node/src/indexing/DecoratorBase.ts +++ b/plugins/search-backend-node/src/indexing/DecoratorBase.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { assertError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; import { IndexableDocument } from '@backstage/plugin-search-common'; import { Transform } from 'node:stream'; @@ -60,8 +60,7 @@ export abstract class DecoratorBase extends Transform { await this.initialize(); done(); } catch (e) { - assertError(e); - done(e); + done(toError(e)); } } @@ -96,8 +95,7 @@ export abstract class DecoratorBase extends Transform { this.push(decorated); done(); } catch (e) { - assertError(e); - done(e); + done(toError(e)); } } @@ -110,8 +108,7 @@ export abstract class DecoratorBase extends Transform { await this.finalize(); done(); } catch (e) { - assertError(e); - done(e); + done(toError(e)); } } } diff --git a/plugins/techdocs-backend/src/DocsBuilder/builder.ts b/plugins/techdocs-backend/src/DocsBuilder/builder.ts index 87fb539ce1..f481b4e23c 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/builder.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/builder.ts @@ -19,7 +19,7 @@ import { stringifyEntityRef, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { assertError, isError } from '@backstage/errors'; +import { isError, toError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { GeneratorBase, @@ -225,9 +225,8 @@ export class DocsBuilder { // Not a blocker hence no need to await this. fs.remove(preparedDir); } catch (error) { - assertError(error); this.logger.debug( - `Error removing prepared directory ${error.message}`, + `Error removing prepared directory ${toError(error).message}`, ); } } @@ -238,9 +237,8 @@ export class DocsBuilder { // Not a blocker hence no need to await this. fs.remove(outputDir); } catch (error) { - assertError(error); this.logger.debug( - `Error removing generated directory ${error.message}`, + `Error removing generated directory ${toError(error).message}`, ); } } diff --git a/plugins/techdocs-backend/src/cache/TechDocsCache.ts b/plugins/techdocs-backend/src/cache/TechDocsCache.ts index d5cbdaae36..9a321f6ed7 100644 --- a/plugins/techdocs-backend/src/cache/TechDocsCache.ts +++ b/plugins/techdocs-backend/src/cache/TechDocsCache.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { assertError, CustomErrorBase } from '@backstage/errors'; +import { CustomErrorBase, toError } from '@backstage/errors'; import { Config, readDurationFromConfig } from '@backstage/config'; import { CacheService, LoggerService } from '@backstage/backend-plugin-api'; import { durationToMilliseconds } from '@backstage/types'; @@ -77,9 +77,9 @@ export class TechDocsCache { this.logger.debug(`Cache miss: ${path}`); return response; } catch (e) { - assertError(e); - this.logger.warn(`Error getting cache entry ${path}: ${e.message}`); - this.logger.debug(e.message, e); + const error = toError(e); + this.logger.warn(`Error getting cache entry ${path}: ${error.message}`); + this.logger.debug(error.message, error); return undefined; } } diff --git a/plugins/techdocs-backend/src/service/DocsSynchronizer.ts b/plugins/techdocs-backend/src/service/DocsSynchronizer.ts index 4827a94f6a..0104ac7500 100644 --- a/plugins/techdocs-backend/src/service/DocsSynchronizer.ts +++ b/plugins/techdocs-backend/src/service/DocsSynchronizer.ts @@ -20,7 +20,7 @@ import { stringifyEntityRef, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { assertError, NotFoundError } from '@backstage/errors'; +import { NotFoundError, toError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { GeneratorBuilder, @@ -146,13 +146,13 @@ export class DocsSynchronizer { return; } } catch (e) { - assertError(e); + const buildError = toError(e); const msg = `Failed to build the docs page for entity ${stringifyEntityRef( entity, - )}: ${e.message}`; + )}: ${buildError.message}`; taskLogger.error(msg); - this.logger.error(msg, e); - error(e); + this.logger.error(msg, buildError); + error(buildError); return; } @@ -241,10 +241,9 @@ export class DocsSynchronizer { finish({ updated: false }); } } catch (e) { - assertError(e); // In case of error, log and allow the user to go about their business. this.logger.error( - `Error syncing cache for ${entityTripletPath}: ${e.message}`, + `Error syncing cache for ${entityTripletPath}: ${toError(e).message}`, ); finish({ updated: false }); } finally { diff --git a/plugins/techdocs-node/src/stages/generate/helpers.ts b/plugins/techdocs-node/src/stages/generate/helpers.ts index caecb47524..59e51ba9a6 100644 --- a/plugins/techdocs-node/src/stages/generate/helpers.ts +++ b/plugins/techdocs-node/src/stages/generate/helpers.ts @@ -15,9 +15,8 @@ */ import { isChildPath, LoggerService } from '@backstage/backend-plugin-api'; -import { NotAllowedError } from '@backstage/errors'; import { Entity } from '@backstage/catalog-model'; -import { assertError, ForwardedError } from '@backstage/errors'; +import { ForwardedError, NotAllowedError, toError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { SpawnOptionsWithoutStdio, spawn } from 'node:child_process'; import fs from 'fs-extra'; @@ -442,8 +441,9 @@ export const createOrUpdateMetadata = async ( try { json = await fs.readJson(techdocsMetadataPath); } catch (err) { - assertError(err); - const message = `Invalid JSON at ${techdocsMetadataPath} with error ${err.message}`; + const message = `Invalid JSON at ${techdocsMetadataPath} with error ${ + toError(err).message + }`; logger.error(message); throw new Error(message); } @@ -457,9 +457,10 @@ export const createOrUpdateMetadata = async ( file.replace(`${techdocsMetadataDir}${path.sep}`, ''), ); } catch (err) { - assertError(err); json.files = []; - logger.warn(`Unable to add files list to metadata: ${err.message}`); + logger.warn( + `Unable to add files list to metadata: ${toError(err).message}`, + ); } await fs.writeJson(techdocsMetadataPath, json); diff --git a/plugins/techdocs-node/src/stages/generate/mkdocsPatchers.ts b/plugins/techdocs-node/src/stages/generate/mkdocsPatchers.ts index d676237b01..c8cd7e32f6 100644 --- a/plugins/techdocs-node/src/stages/generate/mkdocsPatchers.ts +++ b/plugins/techdocs-node/src/stages/generate/mkdocsPatchers.ts @@ -21,7 +21,7 @@ import { getRepoUrlFromLocationAnnotation, MKDOCS_SCHEMA, } from './helpers'; -import { assertError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { LoggerService } from '@backstage/backend-plugin-api'; @@ -45,9 +45,10 @@ const patchMkdocsFile = async ( try { mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8'); } catch (error) { - assertError(error); logger.warn( - `Could not read MkDocs YAML config file ${mkdocsYmlPath} before running the generator: ${error.message}`, + `Could not read MkDocs YAML config file ${mkdocsYmlPath} before running the generator: ${ + toError(error).message + }`, ); return; } @@ -62,9 +63,10 @@ const patchMkdocsFile = async ( throw new Error('Bad YAML format.'); } } catch (error) { - assertError(error); logger.warn( - `Error in parsing YAML at ${mkdocsYmlPath} before running the generator. ${error.message}`, + `Error in parsing YAML at ${mkdocsYmlPath} before running the generator. ${ + toError(error).message + }`, ); return; } @@ -80,9 +82,10 @@ const patchMkdocsFile = async ( ); } } catch (error) { - assertError(error); logger.warn( - `Could not write to ${mkdocsYmlPath} after updating it before running the generator. ${error.message}`, + `Could not write to ${mkdocsYmlPath} after updating it before running the generator. ${ + toError(error).message + }`, ); return; } diff --git a/plugins/techdocs-node/src/stages/prepare/url.ts b/plugins/techdocs-node/src/stages/prepare/url.ts index 260ed454e8..3b5b1f6d29 100644 --- a/plugins/techdocs-node/src/stages/prepare/url.ts +++ b/plugins/techdocs-node/src/stages/prepare/url.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { assertError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; import { Entity } from '@backstage/catalog-model'; import { getDocFilesFromRepository } from '../../helpers'; import { @@ -62,13 +62,13 @@ export class UrlPreparer implements PreparerBase { logger: this.logger, }); } catch (error) { - assertError(error); + const err = toError(error); // NotModifiedError means that etag based cache is still valid. - if (error.name === 'NotModifiedError') { + if (err.name === 'NotModifiedError') { this.logger.debug(`Cache is valid for etag ${options?.etag}`); } else { this.logger.debug( - `Unable to fetch files for building docs ${error.message}`, + `Unable to fetch files for building docs ${err.message}`, ); } diff --git a/plugins/techdocs-node/src/stages/publish/awsS3.ts b/plugins/techdocs-node/src/stages/publish/awsS3.ts index b17125b6ba..28c0529ea1 100644 --- a/plugins/techdocs-node/src/stages/publish/awsS3.ts +++ b/plugins/techdocs-node/src/stages/publish/awsS3.ts @@ -15,7 +15,7 @@ */ import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { assertError, ForwardedError } from '@backstage/errors'; +import { ForwardedError, toError } from '@backstage/errors'; // Maximum size in bytes for a single upload part (5MB) const MAX_SINGLE_UPLOAD_BYTES = 5 * 1024 * 1024; @@ -491,9 +491,10 @@ export class AwsS3Publish implements PublisherBase { .map(f => f.Key || '') .filter(f => !!f); } catch (e) { - assertError(e); this.logger.error( - `Unable to list files for Entity ${entity.metadata.name}: ${e.message}`, + `Unable to list files for Entity ${entity.metadata.name}: ${ + toError(e).message + }`, ); } @@ -708,9 +709,8 @@ export class AwsS3Publish implements PublisherBase { resolve(techdocsMetadata); } catch (err) { - assertError(err); - this.logger.error(err.message); - reject(new Error(err.message)); + this.logger.error(toError(err).message); + reject(new Error(toError(err).message)); } }); } catch (e) { @@ -769,9 +769,10 @@ export class AwsS3Publish implements PublisherBase { }) .pipe(res); } catch (err) { - assertError(err); this.logger.warn( - `TechDocs S3 router failed to serve static files from bucket ${this.bucketName} at key ${filePath}: ${err.message}`, + `TechDocs S3 router failed to serve static files from bucket ${ + this.bucketName + } at key ${filePath}: ${toError(err).message}`, ); res.status(404).send('File Not Found'); } @@ -823,8 +824,7 @@ export class AwsS3Publish implements PublisherBase { try { newPath = lowerCaseEntityTripletInStoragePath(file); } catch (e) { - assertError(e); - this.logger.warn(e.message); + this.logger.warn(toError(e).message); return; } @@ -852,8 +852,9 @@ export class AwsS3Publish implements PublisherBase { ); } } catch (e) { - assertError(e); - this.logger.warn(`Unable to migrate ${file}: ${e.message}`); + this.logger.warn( + `Unable to migrate ${file}: ${toError(e).message}`, + ); } }, f), ), diff --git a/plugins/techdocs-node/src/stages/publish/azureBlobStorage.ts b/plugins/techdocs-node/src/stages/publish/azureBlobStorage.ts index 9202307bd4..1a7e10aa61 100644 --- a/plugins/techdocs-node/src/stages/publish/azureBlobStorage.ts +++ b/plugins/techdocs-node/src/stages/publish/azureBlobStorage.ts @@ -21,7 +21,7 @@ import { } from '@azure/storage-blob'; import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { assertError, ForwardedError } from '@backstage/errors'; +import { ForwardedError, toError } from '@backstage/errors'; import express from 'express'; import JSON5 from 'json5'; import limiterFactory from 'p-limit'; @@ -152,8 +152,9 @@ export class AzureBlobStoragePublish implements PublisherBase { ); } } catch (e) { - assertError(e); - this.logger.error(`from Azure Blob Storage client library: ${e.message}`); + this.logger.error( + `from Azure Blob Storage client library: ${toError(e).message}`, + ); } this.logger.error( @@ -190,9 +191,10 @@ export class AzureBlobStoragePublish implements PublisherBase { maxPageSize: BATCH_CONCURRENCY, }); } catch (e) { - assertError(e); this.logger.error( - `Unable to list files for Entity ${entity.metadata.name}: ${e.message}`, + `Unable to list files for Entity ${entity.metadata.name}: ${ + toError(e).message + }`, ); } @@ -421,8 +423,7 @@ export class AzureBlobStoragePublish implements PublisherBase { try { newPath = lowerCaseEntityTripletInStoragePath(originalPath); } catch (e) { - assertError(e); - this.logger.warn(e.message); + this.logger.warn(toError(e).message); return; } @@ -431,8 +432,9 @@ export class AzureBlobStoragePublish implements PublisherBase { this.logger.debug(`Migrating ${originalPath}`); await this.renameBlob(originalPath, newPath, removeOriginal); } catch (e) { - assertError(e); - this.logger.warn(`Unable to migrate ${originalPath}: ${e.message}`); + this.logger.warn( + `Unable to migrate ${originalPath}: ${toError(e).message}`, + ); } } diff --git a/plugins/techdocs-node/src/stages/publish/googleStorage.ts b/plugins/techdocs-node/src/stages/publish/googleStorage.ts index 0bbc0164b1..7f94bb54da 100644 --- a/plugins/techdocs-node/src/stages/publish/googleStorage.ts +++ b/plugins/techdocs-node/src/stages/publish/googleStorage.ts @@ -15,7 +15,7 @@ */ import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { assertError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; import { File, FileExistsResponse, @@ -149,14 +149,13 @@ export class GoogleGCSPublish implements PublisherBase { isAvailable: true, }; } catch (err) { - assertError(err); this.logger.error( `Could not retrieve metadata about the GCS bucket ${this.bucketName}. ` + 'Make sure the bucket exists. Also make sure that authentication is setup either by explicitly defining ' + 'techdocs.publisher.googleGcs.credentials in app config or by using environment variables. ' + 'Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage', ); - this.logger.error(`from GCS client library: ${err.message}`); + this.logger.error(`from GCS client library: ${toError(err).message}`); return { isAvailable: false }; } @@ -186,9 +185,10 @@ export class GoogleGCSPublish implements PublisherBase { ); existingFiles = await this.getFilesForFolder(remoteFolder); } catch (e) { - assertError(e); this.logger.error( - `Unable to list files for Entity ${entity.metadata.name}: ${e.message}`, + `Unable to list files for Entity ${entity.metadata.name}: ${ + toError(e).message + }`, ); } diff --git a/plugins/techdocs-node/src/stages/publish/migrations/GoogleMigration.ts b/plugins/techdocs-node/src/stages/publish/migrations/GoogleMigration.ts index 6d95af6341..73089e45a6 100644 --- a/plugins/techdocs-node/src/stages/publish/migrations/GoogleMigration.ts +++ b/plugins/techdocs-node/src/stages/publish/migrations/GoogleMigration.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { assertError } from '@backstage/errors'; +import { toError } from '@backstage/errors'; import { File } from '@google-cloud/storage'; import { Writable } from 'node:stream'; import { lowerCaseEntityTripletInStoragePath } from '../helpers'; @@ -47,8 +47,7 @@ export class MigrateWriteStream extends Writable { try { newFile = lowerCaseEntityTripletInStoragePath(file.name); } catch (e) { - assertError(e); - this.logger.warn(e.message); + this.logger.warn(toError(e).message); next(); return; } diff --git a/plugins/techdocs-node/src/stages/publish/openStackSwift.ts b/plugins/techdocs-node/src/stages/publish/openStackSwift.ts index a8740942fd..b718ba6246 100644 --- a/plugins/techdocs-node/src/stages/publish/openStackSwift.ts +++ b/plugins/techdocs-node/src/stages/publish/openStackSwift.ts @@ -36,7 +36,7 @@ import { ReadinessResponse, TechDocsMetadata, } from './types'; -import { assertError, ForwardedError } from '@backstage/errors'; +import { ForwardedError, toError } from '@backstage/errors'; import { LoggerService } from '@backstage/backend-plugin-api'; const streamToBuffer = (stream: Stream | Readable): Promise => { @@ -129,8 +129,9 @@ export class OpenStackSwiftPublish implements PublisherBase { isAvailable: false, }; } catch (err) { - assertError(err); - this.logger.error(`from OpenStack client library: ${err.message}`); + this.logger.error( + `from OpenStack client library: ${toError(err).message}`, + ); return { isAvailable: false, }; @@ -221,9 +222,8 @@ export class OpenStackSwiftPublish implements PublisherBase { resolve(techdocsMetadata); } catch (err) { - assertError(err); - this.logger.error(err.message); - reject(new Error(err.message)); + this.logger.error(toError(err).message); + reject(new Error(toError(err).message)); } } else { reject({ @@ -264,9 +264,10 @@ export class OpenStackSwiftPublish implements PublisherBase { res.send(await streamToBuffer(stream)); } catch (err) { - assertError(err); this.logger.warn( - `TechDocs OpenStack swift router failed to serve content from container ${this.containerName} at path ${filePath}: ${err.message}`, + `TechDocs OpenStack swift router failed to serve content from container ${ + this.containerName + } at path ${filePath}: ${toError(err).message}`, ); res.status(404).send('File Not Found'); } @@ -296,8 +297,7 @@ export class OpenStackSwiftPublish implements PublisherBase { } return false; } catch (err) { - assertError(err); - this.logger.warn(err.message); + this.logger.warn(toError(err).message); return false; } } @@ -316,8 +316,7 @@ export class OpenStackSwiftPublish implements PublisherBase { try { newPath = lowerCaseEntityTripletInStoragePath(file); } catch (e) { - assertError(e); - this.logger.warn(e.message); + this.logger.warn(toError(e).message); return; } @@ -338,8 +337,9 @@ export class OpenStackSwiftPublish implements PublisherBase { await this.storageClient.delete(this.containerName, file); } } catch (e) { - assertError(e); - this.logger.warn(`Unable to migrate ${file}: ${e.message}`); + this.logger.warn( + `Unable to migrate ${file}: ${toError(e).message}`, + ); } }, f), ),