@mtth/stl-errors 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +19 -0
- package/README.md +36 -0
- package/lib/cause.d.ts +44 -0
- package/lib/cause.js +104 -0
- package/lib/common.d.ts +7 -0
- package/lib/common.js +43 -0
- package/lib/factories.d.ts +98 -0
- package/lib/factories.js +197 -0
- package/lib/index.d.ts +108 -0
- package/lib/index.js +197 -0
- package/lib/status.d.ts +191 -0
- package/lib/status.js +238 -0
- package/lib/types.d.ts +75 -0
- package/lib/types.js +6 -0
- package/package.json +28 -0
- package/resources/schemas/error-status.yaml +17 -0
- package/resources/schemas/error.yaml +13 -0
- package/resources/schemas/failure.yaml +11 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2025, Matthieu Monsch.
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
5
|
+
the Software without restriction, including without limitation the rights to
|
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
7
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
8
|
+
so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Standard errors
|
|
2
|
+
|
|
3
|
+
## Motivation
|
|
4
|
+
|
|
5
|
+
Good error handling is a prerequisite for good telemetry. To help with this,
|
|
6
|
+
`@mtth/stl-errors` provides a simple `StandardError` interface which exposes
|
|
7
|
+
powerful building blocks:
|
|
8
|
+
|
|
9
|
+
+ Namespaced error codes;
|
|
10
|
+
+ Causal chains;
|
|
11
|
+
+ Optional structured data.
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
Standard errors are best created via `errorFactories` which provides type-safe
|
|
16
|
+
error creation functions along with their codes:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import {errorFactories} from '@mtth/stl-errors';
|
|
20
|
+
|
|
21
|
+
const [errors, codes] = errorFactories({
|
|
22
|
+
definitions: {
|
|
23
|
+
invalidFoo: (foo: string) => ({
|
|
24
|
+
message: `The input foo ${foo} was invalid`,
|
|
25
|
+
tags: {foo},
|
|
26
|
+
}),
|
|
27
|
+
missingBar: 'The bar was missing',
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Error with code `ERR_INVALID_FOO` (`codes.InvalidFoo`).
|
|
32
|
+
const err1 = errors.invalidFoo('fff');
|
|
33
|
+
|
|
34
|
+
// Error with code `ERR_MISSING_BAR` (`codes.MissingBar`).
|
|
35
|
+
const err2 = errors.missingBar();
|
|
36
|
+
```
|
package/lib/cause.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ErrorCode, ErrorTags, StandardError, TaggedErrorCode } from './types.js';
|
|
2
|
+
export type CauseExtractor = (err: unknown) => ReadonlyArray<unknown> | undefined;
|
|
3
|
+
export interface ErrorMatch<V, E = unknown> {
|
|
4
|
+
readonly value: V;
|
|
5
|
+
readonly error: E;
|
|
6
|
+
}
|
|
7
|
+
export declare class ErrorFinder {
|
|
8
|
+
readonly extractors: CauseExtractor[];
|
|
9
|
+
constructor(extractors: ReadonlyArray<CauseExtractor>);
|
|
10
|
+
/** DFS on causes. */
|
|
11
|
+
private walk;
|
|
12
|
+
find<V, E = unknown>(root: unknown, fn: (e: unknown) => V | undefined): ErrorMatch<V, E> | undefined;
|
|
13
|
+
}
|
|
14
|
+
export declare function errorCauseExtractor(err: unknown): ReadonlyArray<unknown> | undefined;
|
|
15
|
+
export declare function statusErrorCauseExtractor(err: unknown): ReadonlyArray<unknown> | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Resets the list of global extractors used by `findError` and similar methods.
|
|
18
|
+
* The default finder contains only the `libraryCauseExtractor`. This function
|
|
19
|
+
* returns the previous extractors so that they can be restored.
|
|
20
|
+
*/
|
|
21
|
+
export declare function setCauseExtractors(newExtractors: ReadonlyArray<CauseExtractor>): ReadonlyArray<CauseExtractor>;
|
|
22
|
+
/** Finds an error using the current list of global extractors. */
|
|
23
|
+
export declare function findError<V, E = unknown>(root: unknown, fn: (err: unknown) => V | undefined): ErrorMatch<V, E> | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Returns the first code contained by this error. This is useful for example to
|
|
26
|
+
* "see through" status errors.
|
|
27
|
+
*/
|
|
28
|
+
export declare function findErrorCode(root: unknown): ErrorCode | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Minimal interface to allow passing both sets and maps as error code arguments
|
|
31
|
+
* in `findErrorWithCode` and efficiently implement the corresponding check.
|
|
32
|
+
*/
|
|
33
|
+
export interface ErrorCodeCollection {
|
|
34
|
+
has(code: ErrorCode): boolean;
|
|
35
|
+
}
|
|
36
|
+
/** Convenience method to find a internal error by code. */
|
|
37
|
+
export declare function findErrorWithCode<T extends ErrorTags>(root: unknown, code: TaggedErrorCode<T>): ErrorMatch<TaggedErrorCode<T>, StandardError<T>> | undefined;
|
|
38
|
+
export declare function findErrorWithCode<E = StandardError>(root: unknown, code: ErrorCode | ErrorCodeCollection): ErrorMatch<ErrorCode, E> | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Returns a set with all error codes explicitly found in an error's causal
|
|
41
|
+
* chain. Note that unlike with `errorCode`, implicit `ERR_INTERNAL` codes are
|
|
42
|
+
* not added here.
|
|
43
|
+
*/
|
|
44
|
+
export declare function collectErrorCodes(root: unknown): ReadonlySet<ErrorCode>;
|
package/lib/cause.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { errorCode, isStandardError } from './factories.js';
|
|
2
|
+
import { isStatusError } from './status.js';
|
|
3
|
+
export class ErrorFinder {
|
|
4
|
+
constructor(extractors) {
|
|
5
|
+
this.extractors = [...extractors];
|
|
6
|
+
}
|
|
7
|
+
/** DFS on causes. */
|
|
8
|
+
*walk(err) {
|
|
9
|
+
yield err;
|
|
10
|
+
for (const fn of this.extractors) {
|
|
11
|
+
const causes = fn(err);
|
|
12
|
+
if (causes) {
|
|
13
|
+
for (const cause of causes) {
|
|
14
|
+
yield* this.walk(cause);
|
|
15
|
+
}
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
find(root, fn) {
|
|
21
|
+
if (!root) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const seen = new WeakSet();
|
|
25
|
+
for (const cause of this.walk(root)) {
|
|
26
|
+
if (cause && typeof cause == 'object') {
|
|
27
|
+
if (seen.has(cause)) {
|
|
28
|
+
// Circular reference.
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
seen.add(cause);
|
|
32
|
+
}
|
|
33
|
+
const val = fn(cause);
|
|
34
|
+
if (val !== undefined) {
|
|
35
|
+
return { value: val, error: cause };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function errorCauseExtractor(err) {
|
|
42
|
+
if (!isStandardError(err)) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const { cause } = err;
|
|
46
|
+
return cause === undefined
|
|
47
|
+
? undefined
|
|
48
|
+
: Array.isArray(cause)
|
|
49
|
+
? cause
|
|
50
|
+
: [cause];
|
|
51
|
+
}
|
|
52
|
+
export function statusErrorCauseExtractor(err) {
|
|
53
|
+
return isStatusError(err) ? [err.contents] : undefined;
|
|
54
|
+
}
|
|
55
|
+
let globalFinder = new ErrorFinder([
|
|
56
|
+
errorCauseExtractor,
|
|
57
|
+
statusErrorCauseExtractor,
|
|
58
|
+
]);
|
|
59
|
+
/**
|
|
60
|
+
* Resets the list of global extractors used by `findError` and similar methods.
|
|
61
|
+
* The default finder contains only the `libraryCauseExtractor`. This function
|
|
62
|
+
* returns the previous extractors so that they can be restored.
|
|
63
|
+
*/
|
|
64
|
+
export function setCauseExtractors(newExtractors) {
|
|
65
|
+
const oldExtractors = globalFinder.extractors;
|
|
66
|
+
globalFinder = new ErrorFinder(newExtractors);
|
|
67
|
+
return oldExtractors;
|
|
68
|
+
}
|
|
69
|
+
/** Finds an error using the current list of global extractors. */
|
|
70
|
+
export function findError(root, fn) {
|
|
71
|
+
return globalFinder.find(root, fn);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Returns the first code contained by this error. This is useful for example to
|
|
75
|
+
* "see through" status errors.
|
|
76
|
+
*/
|
|
77
|
+
export function findErrorCode(root) {
|
|
78
|
+
const match = findError(root, (err) => isStandardError(err) ? err.code : undefined);
|
|
79
|
+
return match?.value;
|
|
80
|
+
}
|
|
81
|
+
export function findErrorWithCode(root, code) {
|
|
82
|
+
return findError(root, (err) => {
|
|
83
|
+
const errCode = errorCode(err);
|
|
84
|
+
if (errCode == null) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
const ok = typeof code == 'string' ? errCode === code : code.has(errCode);
|
|
88
|
+
return ok ? errCode : undefined;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Returns a set with all error codes explicitly found in an error's causal
|
|
93
|
+
* chain. Note that unlike with `errorCode`, implicit `ERR_INTERNAL` codes are
|
|
94
|
+
* not added here.
|
|
95
|
+
*/
|
|
96
|
+
export function collectErrorCodes(root) {
|
|
97
|
+
const codes = new Set();
|
|
98
|
+
findError(root, (err) => {
|
|
99
|
+
if (isStandardError(err)) {
|
|
100
|
+
codes.add(err.code);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
return codes;
|
|
104
|
+
}
|
package/lib/common.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the error message of a standard error, the message property of other
|
|
3
|
+
* objects, the input if it is a string, and nothing otherwise.
|
|
4
|
+
*/
|
|
5
|
+
export declare function errorMessage(err: unknown): string | undefined;
|
|
6
|
+
/** Formats a string. */
|
|
7
|
+
export declare function format(fmt: string, ...args: any[]): string;
|
package/lib/common.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the error message of a standard error, the message property of other
|
|
3
|
+
* objects, the input if it is a string, and nothing otherwise.
|
|
4
|
+
*/
|
|
5
|
+
export function errorMessage(err) {
|
|
6
|
+
if (typeof err == 'string') {
|
|
7
|
+
return err;
|
|
8
|
+
}
|
|
9
|
+
if (!err || typeof err != 'object') {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
return '' + err.message;
|
|
13
|
+
}
|
|
14
|
+
/** Formats a string. */
|
|
15
|
+
export function format(fmt, ...args) {
|
|
16
|
+
const formatPattern = /(%?)(%([jds]))/g;
|
|
17
|
+
if (args.length) {
|
|
18
|
+
fmt = fmt.replace(formatPattern, (match, esc, _ptn, flag) => {
|
|
19
|
+
let arg = args.shift();
|
|
20
|
+
switch (flag) {
|
|
21
|
+
case 's':
|
|
22
|
+
arg = '' + arg;
|
|
23
|
+
break;
|
|
24
|
+
case 'd':
|
|
25
|
+
arg = Number(arg);
|
|
26
|
+
break;
|
|
27
|
+
case 'j':
|
|
28
|
+
arg = JSON.stringify(arg);
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
if (!esc) {
|
|
32
|
+
return arg;
|
|
33
|
+
}
|
|
34
|
+
args.unshift(arg);
|
|
35
|
+
return match;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (args.length) {
|
|
39
|
+
fmt += ' ' + args.join(' ');
|
|
40
|
+
}
|
|
41
|
+
fmt = fmt.replace(/%{2,2}/g, '%');
|
|
42
|
+
return '' + fmt;
|
|
43
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { DeepErrorCodes, ErrorCode, ErrorOptions, ErrorPrefix, ErrorTags, HasErrorTags, StandardError, StandardErrorForCode, TaggedErrorCode } from './types.js';
|
|
2
|
+
/** Returns whether the argument is a standard error. */
|
|
3
|
+
export declare function isStandardError<T extends ErrorTags>(arg: unknown, code: TaggedErrorCode<T>): arg is StandardError<T>;
|
|
4
|
+
export declare function isStandardError(arg: unknown, code?: ErrorCode | ReadonlySet<ErrorCode>): arg is StandardError;
|
|
5
|
+
/** Standard success code to use as counterpart to error codes. */
|
|
6
|
+
export declare const OK_CODE = "OK";
|
|
7
|
+
/**
|
|
8
|
+
* Returns an error's code if it is a standard error and `undefined` otherwise.
|
|
9
|
+
*/
|
|
10
|
+
export declare function errorCode(err: unknown): ErrorCode | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Constructs a new standard error directly from options. In general prefer
|
|
13
|
+
* using error factories. This can be useful when recreating errors from another
|
|
14
|
+
* system (for example to translate remote gRPC errors into local standard
|
|
15
|
+
* ones). This function will throw if `code` is not a valid error code.
|
|
16
|
+
*/
|
|
17
|
+
export declare function newError<T extends ErrorTags>(name: string, code: ErrorCode | string, opts: ErrorOptions & HasErrorTags<T>): StandardError<T>;
|
|
18
|
+
export declare function newError(name: string, code: ErrorCode | string, opts?: ErrorOptions): StandardError;
|
|
19
|
+
/** Combines error codes into a single object. */
|
|
20
|
+
export declare function mergeErrorCodes<O>(obj: O): DeepErrorCodes<O>;
|
|
21
|
+
/**
|
|
22
|
+
* Generates an object containing error factory methods for each of the input
|
|
23
|
+
* codes. This is particularly useful to concisely generate strongly-typed error
|
|
24
|
+
* creation methods and tagged codes.
|
|
25
|
+
*/
|
|
26
|
+
export declare function errorFactories<P extends ErrorPrefix, O extends object>(params: ErrorFactoriesParams<P, O>): [ErrorFactoriesFor<O>, ErrorCodesFor<O>];
|
|
27
|
+
export interface ErrorFactoriesParams<P extends ErrorPrefix, O extends object> {
|
|
28
|
+
readonly definitions: O;
|
|
29
|
+
/** Custom prefix for all defined error codes. The default is `ERR_`. */
|
|
30
|
+
readonly prefix?: P;
|
|
31
|
+
/**
|
|
32
|
+
* Custom error name. The default is inferred from the prefix if present. For
|
|
33
|
+
* example `ERR_FOO_BAR_` would yield name `FooBarError`.
|
|
34
|
+
*/
|
|
35
|
+
readonly name?: string;
|
|
36
|
+
/** Omit stack for all defined errors. */
|
|
37
|
+
readonly omitStack?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export type ErrorFactoriesFor<O> = {
|
|
40
|
+
readonly [K in keyof O]: O[K] extends (...args: infer A) => StandardError<infer T> | (ErrorOptions & HasErrorTags<infer T>) ? (...args: A) => StandardError<T> : O[K] extends (...args: infer A) => ErrorOptions ? (...args: A) => StandardError : O[K] extends string ? () => StandardError : O[K] extends ErrorOptions ? ErrorFactory : never;
|
|
41
|
+
};
|
|
42
|
+
export interface ErrorFactory {
|
|
43
|
+
(opts?: ErrorOptions): StandardError;
|
|
44
|
+
<T extends ErrorTags>(opts: ErrorOptions & HasErrorTags<T>): StandardError<T>;
|
|
45
|
+
}
|
|
46
|
+
export type ErrorCodesFor<O> = {
|
|
47
|
+
readonly [K in keyof O & string as Capitalize<K>]: O[K] extends (...args: any[]) => StandardError<infer T> | (ErrorOptions & HasErrorTags<infer T>) ? TaggedErrorCode<T> : ErrorCode;
|
|
48
|
+
} & ReadonlySet<ErrorCode>;
|
|
49
|
+
/** Standard error factories. */
|
|
50
|
+
export declare const errors: ErrorFactoriesFor<{
|
|
51
|
+
/** Generic internal error. */
|
|
52
|
+
internal: {
|
|
53
|
+
message: string;
|
|
54
|
+
};
|
|
55
|
+
/** Generic illegal state error, used for assertions in particular. */
|
|
56
|
+
illegal: {
|
|
57
|
+
message: string;
|
|
58
|
+
};
|
|
59
|
+
/** Generic invalid argument error. */
|
|
60
|
+
invalid: {
|
|
61
|
+
message: string;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Coerces non standard errors into one. The original value is available via
|
|
65
|
+
* its `cause` and its message, if any, as top-level message. If the input
|
|
66
|
+
* is already a standard error, it is returned unchanged.
|
|
67
|
+
*/
|
|
68
|
+
coerced: (err: unknown) => StandardError<ErrorTags> | {
|
|
69
|
+
message: string | undefined;
|
|
70
|
+
cause: unknown;
|
|
71
|
+
stackFrom: boolean;
|
|
72
|
+
};
|
|
73
|
+
}>, errorCodes: ErrorCodesFor<{
|
|
74
|
+
/** Generic internal error. */
|
|
75
|
+
internal: {
|
|
76
|
+
message: string;
|
|
77
|
+
};
|
|
78
|
+
/** Generic illegal state error, used for assertions in particular. */
|
|
79
|
+
illegal: {
|
|
80
|
+
message: string;
|
|
81
|
+
};
|
|
82
|
+
/** Generic invalid argument error. */
|
|
83
|
+
invalid: {
|
|
84
|
+
message: string;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Coerces non standard errors into one. The original value is available via
|
|
88
|
+
* its `cause` and its message, if any, as top-level message. If the input
|
|
89
|
+
* is already a standard error, it is returned unchanged.
|
|
90
|
+
*/
|
|
91
|
+
coerced: (err: unknown) => StandardError<ErrorTags> | {
|
|
92
|
+
message: string | undefined;
|
|
93
|
+
cause: unknown;
|
|
94
|
+
stackFrom: boolean;
|
|
95
|
+
};
|
|
96
|
+
}>;
|
|
97
|
+
/** Convenience type for standard errors with code `ERR_COERCED`. */
|
|
98
|
+
export type CoercedError = StandardErrorForCode<typeof errorCodes.Coerced>;
|
package/lib/factories.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { constantCase, pascalCase } from 'change-case';
|
|
2
|
+
import { errorMessage } from './common.js';
|
|
3
|
+
const standardErrorMarker = '@mtth/stl-errors:StandardError+v1';
|
|
4
|
+
export function isStandardError(arg, code) {
|
|
5
|
+
const err = arg;
|
|
6
|
+
if (!err?.[standardErrorMarker]) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
if (!code) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
return typeof code == 'string' ? code === err.code : code.has(err.code);
|
|
13
|
+
}
|
|
14
|
+
/** Standard success code to use as counterpart to error codes. */
|
|
15
|
+
export const OK_CODE = 'OK';
|
|
16
|
+
/**
|
|
17
|
+
* Returns an error's code if it is a standard error and `undefined` otherwise.
|
|
18
|
+
*/
|
|
19
|
+
export function errorCode(err) {
|
|
20
|
+
return isStandardError(err) ? err.code : undefined;
|
|
21
|
+
}
|
|
22
|
+
export function newError(name, code, opts) {
|
|
23
|
+
return new RealStandardError(name, code, opts);
|
|
24
|
+
}
|
|
25
|
+
/** Base class for standard errors. */
|
|
26
|
+
class RealStandardError extends Error {
|
|
27
|
+
constructor(name, code, opts) {
|
|
28
|
+
assertUndefined(codeValidationError(code));
|
|
29
|
+
const msg = opts?.message;
|
|
30
|
+
const limit = Error.stackTraceLimit;
|
|
31
|
+
if (opts?.stackFrom === false) {
|
|
32
|
+
Error.stackTraceLimit = 0;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
super(msg);
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
Error.stackTraceLimit = limit;
|
|
39
|
+
}
|
|
40
|
+
if (typeof Error.captureStackTrace == 'function' &&
|
|
41
|
+
typeof opts?.stackFrom == 'function') {
|
|
42
|
+
Error.captureStackTrace(this, opts?.stackFrom);
|
|
43
|
+
}
|
|
44
|
+
Object.defineProperty(this, standardErrorMarker, { value: true });
|
|
45
|
+
if (name) {
|
|
46
|
+
// Non-enumerable name.
|
|
47
|
+
Object.defineProperty(this, 'name', { value: name });
|
|
48
|
+
}
|
|
49
|
+
this.code = code; // Validated above.
|
|
50
|
+
this.tags = opts?.tags ?? {};
|
|
51
|
+
let cause;
|
|
52
|
+
if (opts && Array.isArray(opts.cause)) {
|
|
53
|
+
const errs = opts.cause.filter((e) => e);
|
|
54
|
+
cause = errs.length <= 1 ? errs[0] : errs;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
cause = opts?.cause;
|
|
58
|
+
}
|
|
59
|
+
if (cause) {
|
|
60
|
+
this.cause = cause;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
toJSON() {
|
|
64
|
+
return {
|
|
65
|
+
code: this.code,
|
|
66
|
+
message: this.message ? this.message : undefined,
|
|
67
|
+
tags: Object.keys(this.tags).length ? this.tags : undefined,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const CODE_PREFIX = 'ERR_';
|
|
72
|
+
function codeValidationError(s) {
|
|
73
|
+
if (!s.startsWith(CODE_PREFIX)) {
|
|
74
|
+
return new Error('Invalid error code prefix');
|
|
75
|
+
}
|
|
76
|
+
if (s === CODE_PREFIX) {
|
|
77
|
+
return new Error('Empty error code suffix');
|
|
78
|
+
}
|
|
79
|
+
return suffixValidationError(s.substring(CODE_PREFIX.length));
|
|
80
|
+
}
|
|
81
|
+
const partPattern = /^[A-Z][A-Z0-9]*$/;
|
|
82
|
+
const PART_SEPARATOR = '_';
|
|
83
|
+
function suffixValidationError(s) {
|
|
84
|
+
const parts = s.split(PART_SEPARATOR);
|
|
85
|
+
if (!parts.length) {
|
|
86
|
+
return new Error('Empty error code suffix');
|
|
87
|
+
}
|
|
88
|
+
for (const part of parts) {
|
|
89
|
+
if (!partPattern.test(part)) {
|
|
90
|
+
return new Error('Invalid error code suffix: ' + s);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
function assertUndefined(err) {
|
|
96
|
+
if (err) {
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Combines error codes into a single object. */
|
|
101
|
+
export function mergeErrorCodes(obj) {
|
|
102
|
+
const codes = new Set();
|
|
103
|
+
walk(obj, codes, [codes]);
|
|
104
|
+
return codes;
|
|
105
|
+
function walk(src, dst, sets) {
|
|
106
|
+
for (const [key, val] of Object.entries(src)) {
|
|
107
|
+
if (typeof val == 'string' || val instanceof Set) {
|
|
108
|
+
for (const v of typeof val == 'string' ? [val] : val.values()) {
|
|
109
|
+
assertUnique(v);
|
|
110
|
+
for (const s of sets) {
|
|
111
|
+
s.add(v);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
dst[key] = val;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
const nested = new Set();
|
|
118
|
+
walk(val, nested, [...sets, nested]);
|
|
119
|
+
dst[key] = nested;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function assertUnique(code) {
|
|
124
|
+
if (codes.has(code)) {
|
|
125
|
+
throw errors.illegal({ message: 'Duplicate error code: ' + code });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function upperCaseFirst(s) {
|
|
130
|
+
return s ? s.charAt(0).toUpperCase() + s.slice(1) : '';
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Generates an object containing error factory methods for each of the input
|
|
134
|
+
* codes. This is particularly useful to concisely generate strongly-typed error
|
|
135
|
+
* creation methods and tagged codes.
|
|
136
|
+
*/
|
|
137
|
+
export function errorFactories(params) {
|
|
138
|
+
const { definitions, omitStack } = params;
|
|
139
|
+
const prefix = params.prefix ?? CODE_PREFIX;
|
|
140
|
+
const codes = new Set();
|
|
141
|
+
const factories = Object.create(null);
|
|
142
|
+
for (const [key, val] of Object.entries(definitions)) {
|
|
143
|
+
const code = prefix + constantCase(key);
|
|
144
|
+
assertUndefined(codeValidationError(code));
|
|
145
|
+
const name = params.name ?? inferName(prefix);
|
|
146
|
+
codes[upperCaseFirst(key)] = code;
|
|
147
|
+
codes.add(code);
|
|
148
|
+
function newError(...args) {
|
|
149
|
+
const stackFrom = omitStack ? false : newError;
|
|
150
|
+
let opts;
|
|
151
|
+
if (typeof val == 'function') {
|
|
152
|
+
const ret = val(...args);
|
|
153
|
+
if (isStandardError(ret)) {
|
|
154
|
+
return ret;
|
|
155
|
+
}
|
|
156
|
+
opts = { stackFrom, ...ret };
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
opts =
|
|
160
|
+
typeof val == 'string'
|
|
161
|
+
? { stackFrom, message: val }
|
|
162
|
+
: { stackFrom, ...val };
|
|
163
|
+
const arg = args[0];
|
|
164
|
+
if (arg !== undefined) {
|
|
165
|
+
Object.assign(opts, arg);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return new RealStandardError(name, code, opts);
|
|
169
|
+
}
|
|
170
|
+
factories[key] = newError;
|
|
171
|
+
}
|
|
172
|
+
return [factories, codes];
|
|
173
|
+
}
|
|
174
|
+
const DEFAULT_NAME = 'StandardError';
|
|
175
|
+
function inferName(p) {
|
|
176
|
+
const s = p.substring(CODE_PREFIX.length);
|
|
177
|
+
return s ? pascalCase(s) + 'Error' : DEFAULT_NAME;
|
|
178
|
+
}
|
|
179
|
+
/** Standard error factories. */
|
|
180
|
+
export const [errors, errorCodes] = errorFactories({
|
|
181
|
+
definitions: {
|
|
182
|
+
/** Generic internal error. */
|
|
183
|
+
internal: { message: 'Internal error' },
|
|
184
|
+
/** Generic illegal state error, used for assertions in particular. */
|
|
185
|
+
illegal: { message: 'Illegal state' },
|
|
186
|
+
/** Generic invalid argument error. */
|
|
187
|
+
invalid: { message: 'Invalid argument' },
|
|
188
|
+
/**
|
|
189
|
+
* Coerces non standard errors into one. The original value is available via
|
|
190
|
+
* its `cause` and its message, if any, as top-level message. If the input
|
|
191
|
+
* is already a standard error, it is returned unchanged.
|
|
192
|
+
*/
|
|
193
|
+
coerced: (err) => isStandardError(err)
|
|
194
|
+
? err
|
|
195
|
+
: { message: errorMessage(err), cause: err, stackFrom: false },
|
|
196
|
+
},
|
|
197
|
+
});
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { ErrorStatus, ErrorStatuses, Failure, StatusError } from './status.js';
|
|
2
|
+
import { ErrorCode, ErrorOptions, ErrorTags, StandardError, TaggedErrorCode } from './types.js';
|
|
3
|
+
export { CauseExtractor, collectErrorCodes, errorCauseExtractor, ErrorCodeCollection, ErrorMatch, findError, findErrorCode, findErrorWithCode, setCauseExtractors, statusErrorCauseExtractor, } from './cause.js';
|
|
4
|
+
export { errorMessage } from './common.js';
|
|
5
|
+
export { CoercedError, errorCode, errorCodes, ErrorCodesFor, errorFactories, ErrorFactoriesFor, ErrorFactoriesParams, ErrorFactory, errors, isStandardError, mergeErrorCodes, newError, OK_CODE, } from './factories.js';
|
|
6
|
+
export { ErrorStatus, ErrorStatuses, Failure, failure, isErrorStatus, isInternalProblem, isServerProblem, isStatusError, OK_STATUS, OkStatus, rethrowWithStatus, StatusError, statusError, StatusErrorFactories, StatusErrorFactory, StatusErrorOptions, statusErrors, statusFromGrpcCode, statusFromHttpCode, StatusMapping, StatusProtocol, statusProtocolCode, StatusProtocolCodes, statusToGrpcCode, statusToHttpCode, } from './status.js';
|
|
7
|
+
export { DeepErrorCodes, ErrorCode, ErrorCodes, ErrorOptions, ErrorPrefix, ErrorTags, ErrorTagsFor, HasErrorTags, StandardError, StandardErrorForCode, TaggedErrorCode, } from './types.js';
|
|
8
|
+
/** Asserts the input predicate, throwing `ERR_ILLEGAL` if not. */
|
|
9
|
+
export declare function assert(pred: unknown, fmt: string, ...args: any[]): asserts pred;
|
|
10
|
+
/** Asserts that an error matches a predicate. */
|
|
11
|
+
export declare function assertCause(pred: unknown, err: unknown): asserts pred;
|
|
12
|
+
/** Asserts that an error has a given error code. */
|
|
13
|
+
export declare function assertErrorCode<T extends ErrorTags>(code: TaggedErrorCode<T>, err: unknown): asserts err is StandardError<T>;
|
|
14
|
+
export declare function assertErrorCode(code: ErrorCode | ReadonlySet<ErrorCode>, err: unknown): asserts err is StandardError;
|
|
15
|
+
/** Asserts that the argument's `typeof` matches the given name. */
|
|
16
|
+
export declare function assertType<N extends keyof TypeNames>(name: N, arg: unknown): asserts arg is TypeNames[N];
|
|
17
|
+
interface TypeNames {
|
|
18
|
+
bigint: bigint;
|
|
19
|
+
boolean: boolean;
|
|
20
|
+
function: Function;
|
|
21
|
+
number: number;
|
|
22
|
+
object: {} | null;
|
|
23
|
+
string: string;
|
|
24
|
+
symbol: symbol;
|
|
25
|
+
undefined: undefined;
|
|
26
|
+
}
|
|
27
|
+
/** Throws `ERR_ILLEGAL` when predicates are not met. */
|
|
28
|
+
export type StrictChecker<R> = (arg: unknown) => R;
|
|
29
|
+
/** Extends `StrictChecker` with a convenience method to skip missing values. */
|
|
30
|
+
export interface Checker<R> extends StrictChecker<R> {
|
|
31
|
+
/**
|
|
32
|
+
* Transforms `null` and `undefined` values into `undefined` rather than
|
|
33
|
+
* throwing.
|
|
34
|
+
*/
|
|
35
|
+
readonly orAbsent: StrictChecker<R | undefined>;
|
|
36
|
+
}
|
|
37
|
+
export declare function newChecker<R>(expected: string, pred: (arg: unknown) => unknown): Checker<R>;
|
|
38
|
+
export declare const check: {
|
|
39
|
+
readonly isString: Checker<string>;
|
|
40
|
+
readonly isNumber: Checker<number>;
|
|
41
|
+
readonly isNumeric: Checker<number>;
|
|
42
|
+
readonly isInteger: Checker<number>;
|
|
43
|
+
readonly isNonNegativeInteger: Checker<number>;
|
|
44
|
+
readonly isBoolean: Checker<boolean>;
|
|
45
|
+
readonly isObject: Checker<object>;
|
|
46
|
+
readonly isRecord: Checker<{
|
|
47
|
+
[key: string]: unknown;
|
|
48
|
+
}>;
|
|
49
|
+
readonly isArray: Checker<unknown[]>;
|
|
50
|
+
readonly isBuffer: Checker<Buffer<ArrayBufferLike>>;
|
|
51
|
+
/** Asserts that the input is not null or undefined and returns it. */
|
|
52
|
+
readonly isPresent: <V>(arg: V) => Exclude<V, null | undefined>;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Extracts the first status error from an error's causal chain. The optional
|
|
56
|
+
* statuses argument can be used to automatically promote certain error codes to
|
|
57
|
+
* have a given status. If no match was found, an `UNKNOWN` status error is
|
|
58
|
+
* returned instead.
|
|
59
|
+
*/
|
|
60
|
+
export declare function extractStatusError(root: unknown, statuses?: ErrorStatuses): StatusError;
|
|
61
|
+
/**
|
|
62
|
+
* Walks an error's causal chain to find the first status error and returns its
|
|
63
|
+
* status. If no match is found, returns `UNKNOWN`.
|
|
64
|
+
*/
|
|
65
|
+
export declare function deriveStatus(root: unknown): ErrorStatus;
|
|
66
|
+
/**
|
|
67
|
+
* Derives the best failure from an error. See `extractStatusError` for
|
|
68
|
+
* information on how the failure's underlying status error is extracted. If not
|
|
69
|
+
* status error was present, the root is used.
|
|
70
|
+
*/
|
|
71
|
+
export declare function deriveFailure(root: unknown, opts?: {
|
|
72
|
+
/**
|
|
73
|
+
* Mapping from error code to status use to automatically wrap errors
|
|
74
|
+
* matching one of the codes with the corresponding status.
|
|
75
|
+
*/
|
|
76
|
+
readonly statuses?: ErrorStatuses;
|
|
77
|
+
/**
|
|
78
|
+
* List of functions used to compute the returned failure's annotations. The
|
|
79
|
+
* input error is the one the failure wil be generated from.
|
|
80
|
+
*/
|
|
81
|
+
readonly annotators?: ReadonlyArray<(err: StatusError) => string>;
|
|
82
|
+
}): Failure;
|
|
83
|
+
/** Returns `ERR_INTERNAL` with `UNIMPLEMENTED` status. */
|
|
84
|
+
export declare function unimplemented(...args: any[]): Error;
|
|
85
|
+
/** Returns an `ERR_ILLEGAL` error with `value` tag set to the input. */
|
|
86
|
+
export declare function absurd(val: never): Error;
|
|
87
|
+
/** Returns an `ERR_ILLEGAL` error with `value` tag set to the input. */
|
|
88
|
+
export declare function unexpected(val: unknown): Error;
|
|
89
|
+
/**
|
|
90
|
+
* Returns the `value` tag of an `ERR_ILLEGAL` error. This is typically useful
|
|
91
|
+
* to inspect errors produced by `unexpected` in tests.
|
|
92
|
+
*/
|
|
93
|
+
export declare function illegalErrorValue(err: unknown): unknown | undefined;
|
|
94
|
+
/** Returns an `ERR_ILLEGAL` error. */
|
|
95
|
+
export declare function unreachable(): Error;
|
|
96
|
+
/** Throws `ERR_ILLEGAL`. */
|
|
97
|
+
export declare function fail(opts?: ErrorOptions): never;
|
|
98
|
+
/**
|
|
99
|
+
* Validates the input predicate, similar to `assert` but throwing `ERR_INVALID`
|
|
100
|
+
* with `INVALID_ARGUMENT` status on failure.
|
|
101
|
+
*/
|
|
102
|
+
export declare function validate(pred: unknown, fmt: string, ...args: any[]): asserts pred;
|
|
103
|
+
export declare function validate(pred: unknown, opts: Omit<ErrorOptions, 'message' | 'stackFrom'>, fmt: string, ...args: any[]): asserts pred;
|
|
104
|
+
/**
|
|
105
|
+
* Similar to `assertCause` but does not wrap errors which do not match the
|
|
106
|
+
* predicate. Non-error instances are still coerced.
|
|
107
|
+
*/
|
|
108
|
+
export declare function rethrowUnless(pred: unknown, err: unknown): asserts pred;
|