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
@@ -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);
@@ -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: [],
@@ -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: [],
+5 -5
View File
@@ -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 🤔');
+5 -8
View File
@@ -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}`);
}
+1 -6
View File
@@ -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);
}
});
+2 -3
View File
@@ -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));
}
};
}
+4 -4
View File
@@ -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);
}
+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,
+2 -3
View File
@@ -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));
}
};
}