-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patherrors.ts
More file actions
155 lines (147 loc) · 5.07 KB
/
errors.ts
File metadata and controls
155 lines (147 loc) · 5.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
/**
* @fileoverview Error utilities with cause chain support.
*
* Provides:
* - `isError(value)` — cross-realm-safe Error check (ES2025 `Error.isError`
* when available, `@@toStringTag` fallback otherwise).
* - `errorMessage(value)` — read the message (with cause chain) from any
* caught value, falling back to the shared `UNKNOWN_ERROR` sentinel.
* - `errorStack(value)` — read the stack (with cause chain) from any
* caught value, or `undefined` for non-Errors.
*
* `messageWithCauses` / `stackWithCauses` are re-exported from pony-cause;
* a patched copy recognizes cross-realm Errors via `isError`.
*/
import { UNKNOWN_ERROR } from './constants/core'
import { messageWithCauses, stackWithCauses } from './external/pony-cause'
import {
ObjectPrototypeToString,
StringPrototypeCharCodeAt,
} from './primordials'
export { UNKNOWN_ERROR, messageWithCauses, stackWithCauses }
/**
* Spec-compliant [`Error.isError`](https://tc39.es/ecma262/#sec-error.iserror)
* with a fallback shim for engines that don't ship it yet.
*
* Returns `true` for Errors from any realm (worker threads, vm contexts,
* iframes) — things same-realm `instanceof Error` misses. Plain objects
* with `name` + `message` properties are **not** recognized.
*
* @example
* try {
* await doWork()
* } catch (e) {
* if (isError(e)) {
* logger.error(e.message)
* } else {
* logger.error(String(e))
* }
* }
*/
/**
* `Error.isError` fallback shim — the in-language approximation used
* when the native ES2025 method isn't available.
*
* Exported separately so test suites on engines that ship the native
* method can still exercise the shim branch directly. Consumers should
* prefer {@link isError}, which picks the native method when present.
*/
export function isErrorShim(value: unknown): value is Error {
if (value === null || typeof value !== 'object') {
return false
}
return ObjectPrototypeToString(value) === '[object Error]'
}
/**
* Reference to the native ES2025 `Error.isError` when the running
* engine ships it, otherwise `undefined`. Exposed separately so tests
* and callers can detect the fast-path without re-probing.
*/
export const isErrorBuiltin: ((value: unknown) => value is Error) | undefined =
(Error as unknown as { isError?: (v: unknown) => v is Error }).isError
/**
* Prefer the native ES2025 `Error.isError` when available (exact
* `[[ErrorData]]` slot check, cross-realm-safe); fall back to
* {@link isErrorShim} otherwise.
*/
export const isError: (value: unknown) => value is Error =
isErrorBuiltin ?? isErrorShim
/**
* Narrow a caught value to a Node.js `ErrnoException` — an Error with a
* `.code` string set by libuv/syscall failures (e.g. `'ENOENT'`,
* `'EACCES'`, `'EBUSY'`, `'EPERM'`). Cross-realm safe (builds on
* {@link isError}), and checks that `code` is a string so a merely
* branded Error without a real errno code returns `false`.
*
* @example
* try {
* await fsPromises.readFile(path)
* } catch (e) {
* if (isErrnoException(e) && e.code === 'ENOENT') {
* // … retry, or return default …
* } else {
* throw e
* }
* }
*/
export function isErrnoException(
value: unknown,
): value is NodeJS.ErrnoException {
if (!isError(value)) {
return false
}
const code = (value as { code?: unknown }).code
if (typeof code !== 'string' || code.length === 0) {
return false
}
// libuv and Node.js errno codes always start with an uppercase
// letter — libuv's `UV_E*` (ENOENT, EACCES, EBUSY, EPERM, EEXIST,
// etc. — see include/uv/errno.h) and Node's `ERR_*` family
// (https://nodejs.org/api/errors.html#nodejs-error-codes). Reject
// Errors whose `.code` is lowercase (usually a package-specific tag)
// rather than maintaining an exact allow-list that would drift on
// every Node release.
const first = StringPrototypeCharCodeAt(code, 0)
return first >= 65 /* 'A' */ && first <= 90 /* 'Z' */
}
/**
* Extract a human-readable message from any caught value.
*
* Walks the `cause` chain for Errors (via {@link messageWithCauses});
* coerces primitives and objects to string; returns
* {@link UNKNOWN_ERROR} for `null`, `undefined`, empty strings,
* `[object Object]`, or Errors with no message.
*
* @example
* try {
* await readConfig(path)
* } catch (e) {
* throw new Error(`Failed to read ${path}: ${errorMessage(e)}`, { cause: e })
* }
*/
export function errorMessage(value: unknown): string {
if (isError(value)) {
return messageWithCauses(value) || UNKNOWN_ERROR
}
if (value === null || value === undefined) {
return UNKNOWN_ERROR
}
const s = String(value)
if (s === '' || s === '[object Object]') {
return UNKNOWN_ERROR
}
return s
}
/**
* Extract a stack trace (with causes) from any caught value.
*
* Returns the cause-aware stack via {@link stackWithCauses} for Errors;
* returns `undefined` for non-Error values, so callers can
* `logger.error(msg, { stack: errorStack(e) })` safely.
*/
export function errorStack(value: unknown): string | undefined {
if (isError(value)) {
return stackWithCauses(value)
}
return undefined
}