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
+3
View File
@@ -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;
}
}
+83 -1
View File
@@ -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'");
});
});
+27
View File
@@ -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;
}
+1 -1
View File
@@ -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,