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 <poldsberg@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
+3
-6
@@ -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;
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
+3
-8
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, Promise<'classic' | 'berry'>>();
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 🤔');
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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<YarnVersion> {
|
||||
: 'berry';
|
||||
return { version: versionString, codename };
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
throw new ForwardedError('Failed to determine yarn version', error);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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<void>;
|
||||
@@ -40,8 +40,7 @@ export function lazy<TModule extends object>(
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
exitWithError(error);
|
||||
exitWithError(toError(error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -155,4 +155,7 @@ export class ServiceUnavailableError extends CustomErrorBase {}
|
||||
|
||||
// @public
|
||||
export function stringifyError(error: unknown): string;
|
||||
|
||||
// @public
|
||||
export function toError(value: unknown): ErrorLike;
|
||||
```
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<TModule extends object>(
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
exitWithError(error);
|
||||
exitWithError(toError(error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user