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:
Patrik Oldsberg
2026-04-03 01:33:34 +02:00
parent 3cdf048f77
commit b2319ffe45
70 changed files with 400 additions and 345 deletions
@@ -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);
@@ -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;
}
@@ -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}`,
);
}
@@ -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),
),
@@ -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}`,
);
}
}
@@ -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
}`,
);
}
@@ -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;
}
@@ -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<Buffer> => {
@@ -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),
),