@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/lib/index.js ADDED
@@ -0,0 +1,197 @@
1
+ import { findError } from './cause.js';
2
+ import { errorMessage, format } from './common.js';
3
+ import { errorCode, errorCodes, errors, isStandardError } from './factories.js';
4
+ import { failure, isStatusError, statusError, statusErrors, } from './status.js';
5
+ export { collectErrorCodes, errorCauseExtractor, findError, findErrorCode, findErrorWithCode, setCauseExtractors, statusErrorCauseExtractor, } from './cause.js';
6
+ export { errorMessage } from './common.js';
7
+ export { errorCode, errorCodes, errorFactories, errors, isStandardError, mergeErrorCodes, newError, OK_CODE, } from './factories.js';
8
+ export { failure, isErrorStatus, isInternalProblem, isServerProblem, isStatusError, OK_STATUS, rethrowWithStatus, statusError, statusErrors, statusFromGrpcCode, statusFromHttpCode, statusProtocolCode, statusToGrpcCode, statusToHttpCode, } from './status.js';
9
+ // Assertions
10
+ /** Asserts the input predicate, throwing `ERR_ILLEGAL` if not. */
11
+ export function assert(pred, fmt, ...args) {
12
+ if (pred) {
13
+ return;
14
+ }
15
+ throw errors.illegal({
16
+ message: 'Assertion failed: ' + format(fmt, ...args),
17
+ stackFrom: assert,
18
+ });
19
+ }
20
+ /** Asserts that an error matches a predicate. */
21
+ export function assertCause(pred, err) {
22
+ if (pred) {
23
+ return;
24
+ }
25
+ throw errors.illegal({
26
+ message: 'Cause assertion failed: ' + errorMessage(err),
27
+ cause: err,
28
+ stackFrom: assertCause,
29
+ });
30
+ }
31
+ export function assertErrorCode(code, err) {
32
+ if (isStandardError(err, code)) {
33
+ return;
34
+ }
35
+ throw errors.illegal({
36
+ message: 'Error code assertion failed: ' + errorMessage(err),
37
+ cause: err,
38
+ tags: { want: code, got: errorCode(err) },
39
+ stackFrom: assertErrorCode,
40
+ });
41
+ }
42
+ /** Asserts that the argument's `typeof` matches the given name. */
43
+ export function assertType(name, arg) {
44
+ assert(typeof arg == name, 'Expected type %s but got %j', name, arg);
45
+ }
46
+ export function newChecker(expected, pred) {
47
+ check.orAbsent = orAbsent;
48
+ return check;
49
+ function check(arg, caller) {
50
+ if (caller && (arg === null || arg === undefined)) {
51
+ return undefined;
52
+ }
53
+ assert(pred(arg), 'Expected %s but got %j', expected, arg);
54
+ return arg;
55
+ }
56
+ function orAbsent(arg) {
57
+ return check(arg, orAbsent);
58
+ }
59
+ }
60
+ export const check = {
61
+ isString: newChecker('a string', (a) => typeof a == 'string'),
62
+ isNumber: newChecker('a number', (a) => typeof a == 'number' && !isNaN(a)),
63
+ isNumeric: newChecker('a number', (a) => {
64
+ let n;
65
+ switch (typeof a) {
66
+ case 'number':
67
+ n = a;
68
+ break;
69
+ case 'string':
70
+ n = +a;
71
+ break;
72
+ }
73
+ return n != null && !isNaN(n);
74
+ }),
75
+ isInteger: newChecker('an integer', (a) => typeof a == 'number' && a === (a | 0)),
76
+ isNonNegativeInteger: newChecker('an integer', (a) => typeof a == 'number' && a === (a | 0) && a >= 0),
77
+ isBoolean: newChecker('a boolean', (a) => typeof a == 'boolean'),
78
+ isObject: newChecker('an object', (a) => a && typeof a === 'object'),
79
+ isRecord: newChecker('a record', (a) => a && typeof a === 'object' && !Array.isArray(a)),
80
+ isArray: newChecker('an array', (a) => Array.isArray(a)),
81
+ isBuffer: newChecker('a buffer', (a) => Buffer.isBuffer(a)),
82
+ /** Asserts that the input is not null or undefined and returns it. */
83
+ isPresent(arg) {
84
+ assert(arg != null, 'Absent value');
85
+ return arg;
86
+ },
87
+ };
88
+ // Failures
89
+ /**
90
+ * Extracts the first status error from an error's causal chain. The optional
91
+ * statuses argument can be used to automatically promote certain error codes to
92
+ * have a given status. If no match was found, an `UNKNOWN` status error is
93
+ * returned instead.
94
+ */
95
+ export function extractStatusError(root, statuses) {
96
+ const match = findError(root, (e) => {
97
+ if (isStatusError(e)) {
98
+ return e.status;
99
+ }
100
+ const code = errorCode(e);
101
+ return code == null ? undefined : statuses?.[code];
102
+ });
103
+ if (!match) {
104
+ return statusError('UNKNOWN', root);
105
+ }
106
+ const { error: err, value: status } = match;
107
+ return isStatusError(err) ? err : statusError(status, errors.coerced(err));
108
+ }
109
+ /**
110
+ * Walks an error's causal chain to find the first status error and returns its
111
+ * status. If no match is found, returns `UNKNOWN`.
112
+ */
113
+ export function deriveStatus(root) {
114
+ return extractStatusError(root).status;
115
+ }
116
+ /**
117
+ * Derives the best failure from an error. See `extractStatusError` for
118
+ * information on how the failure's underlying status error is extracted. If not
119
+ * status error was present, the root is used.
120
+ */
121
+ export function deriveFailure(root, opts) {
122
+ const err = extractStatusError(root, opts?.statuses);
123
+ return failure(err, { annotations: opts?.annotators?.map((fn) => fn(err)) });
124
+ }
125
+ // Conveniences
126
+ /** Returns `ERR_INTERNAL` with `UNIMPLEMENTED` status. */
127
+ export function unimplemented(...args) {
128
+ return statusErrors.unimplemented(errors.internal({
129
+ message: 'Unimplemented',
130
+ tags: { arguments: args },
131
+ stackFrom: unimplemented,
132
+ }));
133
+ }
134
+ /** Returns an `ERR_ILLEGAL` error with `value` tag set to the input. */
135
+ export function absurd(val) {
136
+ return errors.illegal({
137
+ message: format('Absurd value: %j', val),
138
+ tags: { value: val },
139
+ stackFrom: absurd,
140
+ });
141
+ }
142
+ /** Returns an `ERR_ILLEGAL` error with `value` tag set to the input. */
143
+ export function unexpected(val) {
144
+ return errors.illegal({
145
+ message: format('Unexpected value: %j', val),
146
+ tags: { value: val },
147
+ stackFrom: unexpected,
148
+ });
149
+ }
150
+ /**
151
+ * Returns the `value` tag of an `ERR_ILLEGAL` error. This is typically useful
152
+ * to inspect errors produced by `unexpected` in tests.
153
+ */
154
+ export function illegalErrorValue(err) {
155
+ assert(isStandardError(err, errorCodes.Illegal), 'Unexpected error: %j', err);
156
+ return err.tags.value;
157
+ }
158
+ /** Returns an `ERR_ILLEGAL` error. */
159
+ export function unreachable() {
160
+ return errors.illegal({ message: 'Unexpected call', stackFrom: unreachable });
161
+ }
162
+ /** Throws `ERR_ILLEGAL`. */
163
+ export function fail(opts) {
164
+ throw errors.illegal({ stackFrom: fail, ...opts });
165
+ }
166
+ export function validate(pred, arg1, ...args) {
167
+ if (pred) {
168
+ return;
169
+ }
170
+ let fmt;
171
+ let opts;
172
+ if (typeof arg1 == 'string') {
173
+ opts = {};
174
+ fmt = arg1;
175
+ }
176
+ else {
177
+ opts = arg1;
178
+ fmt = args.shift();
179
+ }
180
+ const err = errors.invalid({
181
+ ...opts,
182
+ message: fmt == null ? undefined : format(fmt, ...args),
183
+ stackFrom: validate,
184
+ });
185
+ throw statusErrors.invalidArgument(err);
186
+ }
187
+ // Other
188
+ /**
189
+ * Similar to `assertCause` but does not wrap errors which do not match the
190
+ * predicate. Non-error instances are still coerced.
191
+ */
192
+ export function rethrowUnless(pred, err) {
193
+ if (pred) {
194
+ return;
195
+ }
196
+ throw err instanceof Error ? err : errors.coerced(err);
197
+ }
@@ -0,0 +1,191 @@
1
+ import { CoercedError } from './factories.js';
2
+ import { ErrorCode } from './types.js';
3
+ declare const statuses: {
4
+ /** Status representing other errors. */
5
+ readonly UNKNOWN: {
6
+ readonly grpc: 2;
7
+ readonly http: 500;
8
+ };
9
+ /**
10
+ * Generic internal failure status. This status is used by default for
11
+ * standard errors which do not have one set explicitly. In general you
12
+ * shouldn't need to set it except when masking an existing status.
13
+ */
14
+ readonly INTERNAL: {
15
+ readonly grpc: 13;
16
+ readonly http: 500;
17
+ };
18
+ /**
19
+ * The operation is not implemented or is not supported/enabled in this
20
+ * service.
21
+ */
22
+ readonly UNIMPLEMENTED: {
23
+ readonly grpc: 12;
24
+ readonly http: 501;
25
+ };
26
+ /** The service is currently unavailable. */
27
+ readonly UNAVAILABLE: {
28
+ readonly grpc: 14;
29
+ readonly http: 503;
30
+ };
31
+ /** The deadline expired before the operation could complete. */
32
+ readonly DEADLINE_EXCEEDED: {
33
+ readonly grpc: 4;
34
+ readonly http: 504;
35
+ };
36
+ /**
37
+ * The operation was aborted, typically due to a concurrency issue such as a
38
+ * sequencer check failure or transaction abort.
39
+ */
40
+ readonly ABORTED: {
41
+ readonly grpc: 10;
42
+ readonly http: 504;
43
+ };
44
+ /** The client specified an invalid argument. */
45
+ readonly INVALID_ARGUMENT: {
46
+ readonly grpc: 3;
47
+ readonly http: 400;
48
+ };
49
+ /**
50
+ * The request does not have valid authentication credentials for the
51
+ * operation.
52
+ */
53
+ readonly UNAUTHENTICATED: {
54
+ readonly grpc: 16;
55
+ readonly http: 401;
56
+ };
57
+ /** The caller does not have permission to execute the specified operation. */
58
+ readonly PERMISSION_DENIED: {
59
+ readonly grpc: 7;
60
+ readonly http: 403;
61
+ };
62
+ /** Some requested entity (e.g., file or directory) was not found. */
63
+ readonly NOT_FOUND: {
64
+ readonly grpc: 5;
65
+ readonly http: 404;
66
+ };
67
+ /**
68
+ * The entity that a client attempted to create (e.g., file or directory)
69
+ * already exists.
70
+ */
71
+ readonly ALREADY_EXISTS: {
72
+ readonly grpc: 6;
73
+ readonly http: 409;
74
+ };
75
+ /**
76
+ * The operation was rejected because the system is not in a state required
77
+ * for the operation's execution.
78
+ */
79
+ readonly FAILED_PRECONDITION: {
80
+ readonly grpc: 9;
81
+ readonly http: 422;
82
+ };
83
+ /** Some resource has been exhausted. */
84
+ readonly RESOURCE_EXHAUSTED: {
85
+ readonly grpc: 8;
86
+ readonly http: 429;
87
+ };
88
+ /** The operation was cancelled, typically by the caller. */
89
+ readonly CANCELLED: {
90
+ readonly grpc: 1;
91
+ readonly http: 499;
92
+ };
93
+ };
94
+ export type StatusProtocol = 'grpc' | 'http';
95
+ export type ErrorStatus = keyof typeof statuses;
96
+ /**
97
+ * Non-error status, useful for example when including an ok case within
98
+ * aggregations or metrics.
99
+ */
100
+ export declare const OK_STATUS = "OK";
101
+ export type OkStatus = typeof OK_STATUS;
102
+ export declare function isErrorStatus(arg: string): arg is ErrorStatus;
103
+ /** A wrapping error which decorates another with a status. */
104
+ export interface StatusError<E extends Error = Error> extends Error {
105
+ /** The error status. */
106
+ readonly status: ErrorStatus;
107
+ /** The underlying error, to which the status is added. */
108
+ readonly contents: E;
109
+ /**
110
+ * Protocol code overrides. This can be useful when the generic status is not
111
+ * as granular as the underlying protocol's error codes.
112
+ */
113
+ readonly protocolCodes: StatusProtocolCodes;
114
+ }
115
+ export type StatusProtocolCodes = {
116
+ readonly [P in StatusProtocol]?: number;
117
+ };
118
+ export interface StatusErrorOptions {
119
+ readonly protocolCodes?: StatusProtocolCodes;
120
+ }
121
+ /**
122
+ * Returns true iff the status corresponds to an internal issue (500 code). This
123
+ * should be used to avoid surfacing internal error details to clients.
124
+ */
125
+ export declare function isInternalProblem(status: ErrorStatus): boolean;
126
+ /**
127
+ * Returns true iff the status corresponds to a server-side issue. This is all
128
+ * statuses corresponding to 5XX codes, except UNIMPLEMENTED (technically 501).
129
+ */
130
+ export declare function isServerProblem(status: ErrorStatus): boolean;
131
+ export declare function statusError<E>(status: ErrorStatus, err: E, opts?: StatusErrorOptions): StatusError<E extends Error ? E : CoercedError>;
132
+ export type StatusErrorFactory = <E extends Error>(err: E, opts?: StatusErrorOptions) => StatusError<E>;
133
+ export type StatusErrorFactories = {
134
+ readonly [K in Exclude<ErrorStatus, 'UNKNOWN'> as ConstantToCamelCase<K>]: StatusErrorFactory;
135
+ };
136
+ /** Type-level case-change from `CONSTANT_CASE` to `camelCase`. */
137
+ type ConstantToCamelCase<S extends string> = S extends `${infer T}_${infer U}` ? `${Lowercase<T>}${Capitalize<ConstantToCamelCase<U>>}` : Lowercase<S>;
138
+ export declare const statusErrors: StatusErrorFactories;
139
+ export declare function isStatusError(err: unknown): err is StatusError;
140
+ /**
141
+ * Returns the default numeric gRPC code for a given status. See `protocolCodes`
142
+ * for transmitting more granular values.
143
+ */
144
+ export declare function statusToGrpcCode(status: ErrorStatus): number;
145
+ export declare function statusFromGrpcCode(code: number): ErrorStatus | OkStatus;
146
+ /**
147
+ * Returns the default numeric HTTP code for a given status. See `protocolCodes`
148
+ * for transmitting more granular values.
149
+ */
150
+ export declare function statusToHttpCode(status: ErrorStatus): number;
151
+ export declare function statusFromHttpCode(code: number): ErrorStatus | OkStatus;
152
+ /** Returns the best numeric error code for a given error and protocol. */
153
+ export declare function statusProtocolCode(protocol: StatusProtocol, err: StatusError): number;
154
+ export declare function inferErrorStatus(err: unknown): ErrorStatus;
155
+ /** A failure is the public representation of an error. */
156
+ export interface Failure {
157
+ /** Standard error status. */
158
+ readonly status: ErrorStatus;
159
+ /** The error that caused the failure. */
160
+ readonly error: {
161
+ /** Human-readable message about the error. */
162
+ readonly message: string;
163
+ /** Application-specific error code for programmatic handling. */
164
+ readonly code?: string;
165
+ /** Structured metadata. */
166
+ readonly tags?: {
167
+ readonly [key: string]: unknown;
168
+ };
169
+ };
170
+ }
171
+ /** Generates a new failure from a status error. */
172
+ export declare function failure(err: StatusError, opts?: {
173
+ /** Annotations to append to the failure's message */
174
+ readonly annotations?: ReadonlyArray<string>;
175
+ }): Failure;
176
+ /** Mapping from status to error code(s). */
177
+ export type StatusMapping = {
178
+ readonly [S in ErrorStatus]?: ErrorCode | Iterable<ErrorCode>;
179
+ };
180
+ /**
181
+ * Wraps and rethrows an (internal) error with the status specified in the input
182
+ * mapping. If the error is not an internal one or doesn't match, this method
183
+ * rethrows the original error. Note that this method does not walk the error's
184
+ * causal chain to avoid swallowing downstream errors.
185
+ */
186
+ export declare function rethrowWithStatus(err: unknown, mapping: StatusMapping): never;
187
+ /** Map from error code to status. */
188
+ export interface ErrorStatuses {
189
+ readonly [code: ErrorCode]: ErrorStatus;
190
+ }
191
+ export {};
package/lib/status.js ADDED
@@ -0,0 +1,238 @@
1
+ import { camelCase, sentenceCase } from 'change-case';
2
+ import { errorMessage } from './common.js';
3
+ import { errorCode, errorCodes, errors, isStandardError, } from './factories.js';
4
+ // https://grpc.github.io/grpc/core/md_doc_statuscodes.html
5
+ // https://cloud.yandex.com/en/docs/api-design-guide/concepts/errors
6
+ const statuses = {
7
+ // Private (internal) statuses. Their corresponding status errors do not
8
+ // expose any information about their contents when serialized to failures.
9
+ /** Status representing other errors. */
10
+ UNKNOWN: { grpc: 2, http: 500 },
11
+ /**
12
+ * Generic internal failure status. This status is used by default for
13
+ * standard errors which do not have one set explicitly. In general you
14
+ * shouldn't need to set it except when masking an existing status.
15
+ */
16
+ INTERNAL: { grpc: 13, http: 500 },
17
+ // Public statuses
18
+ // 5XX
19
+ /**
20
+ * The operation is not implemented or is not supported/enabled in this
21
+ * service.
22
+ */
23
+ UNIMPLEMENTED: { grpc: 12, http: 501 },
24
+ /** The service is currently unavailable. */
25
+ UNAVAILABLE: { grpc: 14, http: 503 },
26
+ /** The deadline expired before the operation could complete. */
27
+ DEADLINE_EXCEEDED: { grpc: 4, http: 504 },
28
+ /**
29
+ * The operation was aborted, typically due to a concurrency issue such as a
30
+ * sequencer check failure or transaction abort.
31
+ */
32
+ ABORTED: { grpc: 10, http: 504 },
33
+ // 4XX
34
+ /** The client specified an invalid argument. */
35
+ INVALID_ARGUMENT: { grpc: 3, http: 400 },
36
+ /**
37
+ * The request does not have valid authentication credentials for the
38
+ * operation.
39
+ */
40
+ UNAUTHENTICATED: { grpc: 16, http: 401 },
41
+ /** The caller does not have permission to execute the specified operation. */
42
+ PERMISSION_DENIED: { grpc: 7, http: 403 },
43
+ /** Some requested entity (e.g., file or directory) was not found. */
44
+ NOT_FOUND: { grpc: 5, http: 404 },
45
+ /**
46
+ * The entity that a client attempted to create (e.g., file or directory)
47
+ * already exists.
48
+ */
49
+ ALREADY_EXISTS: { grpc: 6, http: 409 },
50
+ /**
51
+ * The operation was rejected because the system is not in a state required
52
+ * for the operation's execution.
53
+ */
54
+ FAILED_PRECONDITION: { grpc: 9, http: 422 },
55
+ /** Some resource has been exhausted. */
56
+ RESOURCE_EXHAUSTED: { grpc: 8, http: 429 },
57
+ /** The operation was cancelled, typically by the caller. */
58
+ CANCELLED: { grpc: 1, http: 499 },
59
+ // TODO: Add data loss status?
60
+ };
61
+ /**
62
+ * Non-error status, useful for example when including an ok case within
63
+ * aggregations or metrics.
64
+ */
65
+ export const OK_STATUS = 'OK';
66
+ function findContents(arg) {
67
+ let err = arg;
68
+ while (isStatusError(err)) {
69
+ err = err.contents;
70
+ }
71
+ return err;
72
+ }
73
+ export function isErrorStatus(arg) {
74
+ return !!statuses[arg];
75
+ }
76
+ const isStatusErrorMarker = '@mtth/stl-errors:isStatusError+v1';
77
+ class RealStatusError extends Error {
78
+ constructor(status, contents, protocolCodes, stackFrom) {
79
+ super(statusErrorMessage(status, contents));
80
+ this.status = status;
81
+ this.contents = contents;
82
+ this.protocolCodes = protocolCodes;
83
+ this.name = 'StatusError';
84
+ Object.defineProperty(this, isStatusErrorMarker, { value: true });
85
+ if (typeof Error.captureStackTrace == 'function') {
86
+ Error.captureStackTrace(this, stackFrom);
87
+ }
88
+ }
89
+ }
90
+ function statusErrorMessage(status, err) {
91
+ let ret = `${sentenceCase(status)} error`;
92
+ if (isInternalProblem(status) || !err) {
93
+ return ret;
94
+ }
95
+ const cause = findContents(err);
96
+ const code = errorCode(cause);
97
+ if (code && code !== errorCodes.Coerced) {
98
+ ret += ` [${code}]`;
99
+ }
100
+ const msg = errorMessage(cause);
101
+ if (msg) {
102
+ if (msg.startsWith(ret)) {
103
+ // Avoid repeating the prefix.
104
+ return msg;
105
+ }
106
+ ret += `: ${msg}`;
107
+ }
108
+ return ret;
109
+ }
110
+ /**
111
+ * Returns true iff the status corresponds to an internal issue (500 code). This
112
+ * should be used to avoid surfacing internal error details to clients.
113
+ */
114
+ export function isInternalProblem(status) {
115
+ return statuses[status].http === 500;
116
+ }
117
+ /**
118
+ * Returns true iff the status corresponds to a server-side issue. This is all
119
+ * statuses corresponding to 5XX codes, except UNIMPLEMENTED (technically 501).
120
+ */
121
+ export function isServerProblem(status) {
122
+ const code = statuses[status].http;
123
+ return code === 500 || code > 501; // UNIMPLEMENTED is not a server problem.
124
+ }
125
+ export function statusError(status, err, opts) {
126
+ const contents = err instanceof Error ? err : errors.coerced(err);
127
+ return new RealStatusError(status, contents, opts?.protocolCodes ?? {}, statusError);
128
+ }
129
+ export const statusErrors = (() => {
130
+ const obj = Object.create(null);
131
+ for (const key of Object.keys(statuses)) {
132
+ const status = key;
133
+ if (status === 'UNKNOWN') {
134
+ continue;
135
+ }
136
+ function newError(err, opts) {
137
+ const codes = opts?.protocolCodes ?? {};
138
+ return new RealStatusError(status, err, codes, newError);
139
+ }
140
+ obj[camelCase(status)] = newError;
141
+ }
142
+ return obj;
143
+ })();
144
+ export function isStatusError(err) {
145
+ return err && err[isStatusErrorMarker];
146
+ }
147
+ const statusesByGrpcCode = new Map(Object.entries(statuses).map(([k, v]) => [v.grpc, k]));
148
+ /**
149
+ * Returns the default numeric gRPC code for a given status. See `protocolCodes`
150
+ * for transmitting more granular values.
151
+ */
152
+ export function statusToGrpcCode(status) {
153
+ return statuses[status].grpc;
154
+ }
155
+ export function statusFromGrpcCode(code) {
156
+ if (code === 0) {
157
+ return OK_STATUS;
158
+ }
159
+ return statusesByGrpcCode.get(code) ?? 'UNKNOWN';
160
+ }
161
+ const statusesByHttpCode = new Map(Object.entries(statuses).map(([k, v]) => [v.http, k]));
162
+ /**
163
+ * Returns the default numeric HTTP code for a given status. See `protocolCodes`
164
+ * for transmitting more granular values.
165
+ */
166
+ export function statusToHttpCode(status) {
167
+ return statuses[status].http;
168
+ }
169
+ export function statusFromHttpCode(code) {
170
+ if (code < 400) {
171
+ return OK_STATUS;
172
+ }
173
+ return statusesByHttpCode.get(code) ?? 'UNKNOWN';
174
+ }
175
+ /** Returns the best numeric error code for a given error and protocol. */
176
+ export function statusProtocolCode(protocol, err) {
177
+ return (err.protocolCodes[protocol] ??
178
+ (protocol === 'grpc' ? statusToGrpcCode : statusToHttpCode)(err.status));
179
+ }
180
+ export function inferErrorStatus(err) {
181
+ return isStatusError(err) ? err.status : 'UNKNOWN';
182
+ }
183
+ /** Generates a new failure from a status error. */
184
+ export function failure(err, opts) {
185
+ let message = err.message.trimEnd();
186
+ if (opts?.annotations) {
187
+ for (const a of opts.annotations) {
188
+ message = annotating(message, a.trim());
189
+ }
190
+ }
191
+ const data = { status: err.status, error: { message } };
192
+ if (isInternalProblem(err.status)) {
193
+ return data;
194
+ }
195
+ const contents = findContents(err);
196
+ if (!contents) {
197
+ return data;
198
+ }
199
+ if (isStandardError(contents)) {
200
+ data.error.code = contents.code;
201
+ const tags = { ...contents.tags };
202
+ if (Object.keys(tags).length) {
203
+ data.error.tags = tags;
204
+ }
205
+ }
206
+ return data;
207
+ }
208
+ const punctuatedPattern = /[.!?]$/;
209
+ function annotating(msg, annotation) {
210
+ return annotation
211
+ ? punctuatedPattern.test(msg)
212
+ ? `${msg} ${annotation}`
213
+ : `${msg}. ${annotation}`
214
+ : msg;
215
+ }
216
+ /**
217
+ * Wraps and rethrows an (internal) error with the status specified in the input
218
+ * mapping. If the error is not an internal one or doesn't match, this method
219
+ * rethrows the original error. Note that this method does not walk the error's
220
+ * causal chain to avoid swallowing downstream errors.
221
+ */
222
+ export function rethrowWithStatus(err, mapping) {
223
+ if (!isStandardError(err)) {
224
+ throw err;
225
+ }
226
+ for (const [status, val] of Object.entries(mapping)) {
227
+ if (!isErrorStatus(status)) {
228
+ throw errors.illegal({ message: 'Invalid status: ' + status });
229
+ }
230
+ const codes = typeof val == 'string' ? [val] : val;
231
+ for (const code of codes) {
232
+ if (code === err.code) {
233
+ throw statusError(status, err);
234
+ }
235
+ }
236
+ }
237
+ throw err;
238
+ }
package/lib/types.d.ts ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Structured code string, useful for programmatic error handling. See also the
3
+ * `TaggedErrorCode` variant which supports strongly-typed error retrieval.
4
+ */
5
+ export type ErrorCode = `ERR_${string}`;
6
+ export type ErrorPrefix = 'ERR_' | `ERR_${string}_`;
7
+ /** An error with associated code and, optionally, cause and structured data. */
8
+ export interface StandardError<T extends ErrorTags = ErrorTags> extends Error, HasErrorTags<T> {
9
+ /**
10
+ * The error's code. This code should uniquely identify the type of failure
11
+ * represented by the error.
12
+ */
13
+ readonly code: ErrorCode;
14
+ /** The underlying cause(s) of the error, if any. */
15
+ readonly cause?: unknown | ReadonlyArray<unknown>;
16
+ }
17
+ /** Convenience type generating an error type from its code. */
18
+ export type StandardErrorForCode<C extends ErrorCode> = StandardError<ErrorTagsFor<C>>;
19
+ /** Error with attached structured metadata. */
20
+ export interface HasErrorTags<T extends ErrorTags> {
21
+ /**
22
+ * Metadata tied for the error. This is particularly useful for programmatic
23
+ * handling of errors.
24
+ */
25
+ readonly tags: T;
26
+ }
27
+ /**
28
+ * Generic structured metadata type. Errors constructed via `errorFactories`
29
+ * will have a more specific type.
30
+ */
31
+ export interface ErrorTags {
32
+ readonly [key: string]: unknown;
33
+ readonly [key: symbol]: unknown;
34
+ }
35
+ /**
36
+ * Virtual (type-level) key used to store tag information. We don't use a symbol
37
+ * to allow type-checking to work across compatible versions of this library.
38
+ */
39
+ declare const errorCodeTag = "@mtth/stl-errors:errorCodeTag+v1";
40
+ /**
41
+ * An error code with attached type information about the error's tags. This
42
+ * information can be picked up during retrieval (e.g. `isStandardError`) to
43
+ * make the type of matching error's tags as specific as possible.
44
+ */
45
+ export type TaggedErrorCode<T extends ErrorTags> = ErrorCode & {
46
+ readonly [errorCodeTag]: T;
47
+ };
48
+ export type ErrorTagsFor<C extends ErrorCode> = C extends TaggedErrorCode<infer T> ? T : ErrorTags;
49
+ /** Standard error creation options. */
50
+ export interface ErrorOptions {
51
+ /** A human readable description of the error. */
52
+ readonly message?: string;
53
+ /** Structured data to attach to the error. */
54
+ readonly tags?: ErrorTags;
55
+ /**
56
+ * Optional underlying error(s) which caused this one. If not an error or
57
+ * array of errors, the cause will be normalized to one.
58
+ */
59
+ readonly cause?: unknown | ReadonlyArray<unknown>;
60
+ /**
61
+ * Advanced option to customize the error's stack trace. Passing in a function
62
+ * will make it start at this frame. Passing in `false` will disable it
63
+ * altogether. `true` (the default) will generate a standard stack trace. Note
64
+ * that errors without a stack trace are ~90% cheaper to create (whether the
65
+ * stack is accessed or not).
66
+ */
67
+ readonly stackFrom?: boolean | Function;
68
+ }
69
+ export type DeepErrorCodes<O> = {
70
+ readonly [K in keyof O]: O[K] extends ErrorCode | ErrorCodes ? O[K] : DeepErrorCodes<O[K]>;
71
+ } & ReadonlySet<ErrorCode>;
72
+ export type ErrorCodes<S extends string = string> = {
73
+ readonly [K in S]: ErrorCode;
74
+ } & ReadonlySet<string>;
75
+ export {};