-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Expand file tree
/
Copy patherrors.ts
More file actions
156 lines (138 loc) · 5.28 KB
/
Copy patherrors.ts
File metadata and controls
156 lines (138 loc) · 5.28 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
156
/**
* Normalizes an unknown caught value into an Error instance.
* Replaces the common `e instanceof Error ? e : new Error(String(e))` pattern in catch clauses.
*/
export function toError(value: unknown): Error {
if (value instanceof Error) return value
if (typeof value === 'string') return new Error(value)
return new Error(String(value))
}
/**
* Extracts a string message from an unknown caught value.
* Use instead of `e instanceof Error ? e.message : 'fallback'` in catch clauses.
*
* - Error instance → `error.message`
* - Non-empty string → the string itself (handles `throw 'msg'` patterns)
* - Otherwise → `fallback` if provided, or `String(value)`
*/
export function getErrorMessage(value: unknown, fallback?: string): string {
if (value instanceof Error) return value.message
if (typeof value === 'string' && value.length > 0) return value
return fallback ?? String(value)
}
/**
* Returns PostgreSQL error code (e.g. `23505` for unique_violation) when present on a thrown value.
* Normalizes common Drizzle / `postgres` driver shapes and walks `cause` chains.
*/
export function getPostgresErrorCode(error: unknown): string | undefined {
return readPgErrorField(error, 'code')
}
/**
* Returns the name of the PostgreSQL constraint that triggered the error (e.g. the unique index
* name on a `23505`), when present on a thrown value. Mirrors the field populated by the
* `postgres` / `pg` drivers, walking `cause` chains the same way as `getPostgresErrorCode`.
*/
export function getPostgresConstraintName(error: unknown): string | undefined {
return readPgErrorField(error, 'constraint_name') ?? readPgErrorField(error, 'constraint')
}
export interface DescribedError {
name: string
message: string
code?: string
errno?: string
syscall?: string
/** `"Name: message"` per link in the `.cause` chain, outermost first. Present only when the chain has more than one link. */
causeChain?: string[]
}
/**
* Always-on diagnostic view of an error and its `.cause` chain.
*
* Reports the fields of the DEEPEST `.cause` link, because a wrapped driver
* error (e.g. Drizzle's `"Failed query: ..."` wrapping an `ECONNRESET`) carries
* the real reason there, not on the outer wrapper. Always returns a populated
* object — including for non-`Error` throws and unclassified errors like
* `AbortError`. Cycle-safe and depth-bounded.
*
* Loggers do not serialize the non-enumerable `Error.prototype.cause`, so pass
* the result as an explicit structured field rather than the raw error.
*
* Bound parameter values are stripped from every reported message: Drizzle's
* `DrizzleQueryError` appends `\nparams: <values>` to the failing SQL, and those
* values are user data that must never reach logs.
*/
export function describeError(error: unknown): DescribedError {
const chain: Error[] = []
const seen = new Set<unknown>()
let current: unknown = error
while (current instanceof Error && !seen.has(current) && chain.length < 10) {
seen.add(current)
chain.push(current)
current = current.cause
}
if (chain.length === 0) {
const normalized = toError(error)
return { name: normalized.name, message: redactBoundParameters(normalized.message) }
}
const deepest = chain[chain.length - 1] as Error & Record<string, unknown>
const asString = (value: unknown): string | undefined =>
typeof value === 'string' ? value : undefined
const code = asString(deepest.code)
const errno = asString(deepest.errno)
const syscall = asString(deepest.syscall)
return {
name: deepest.name,
message: redactBoundParameters(deepest.message),
...(code ? { code } : {}),
...(errno ? { errno } : {}),
...(syscall ? { syscall } : {}),
...(chain.length > 1
? { causeChain: chain.map((e) => `${e.name}: ${redactBoundParameters(e.message)}`) }
: {}),
}
}
/** Replaces a driver-appended `params: <values>` tail with a redaction marker. */
export function redactBoundParameters(message: string): string {
const index = message.indexOf('\nparams:')
return index === -1 ? message : `${message.slice(0, index)}\nparams: [redacted]`
}
/**
* First link in the `.cause` chain (including `error` itself) matching
* `predicate`. Lets a caller recover a specific wrapped error class instead of
* re-parsing a formatted message. Cycle-safe and depth-bounded, mirroring
* {@link describeError}'s walk.
*/
export function findCause<T>(
error: unknown,
predicate: (value: unknown) => value is T
): T | undefined {
const seen = new Set<unknown>()
let current: unknown = error
while (current instanceof Error && !seen.has(current) && seen.size < 10) {
seen.add(current)
if (predicate(current)) return current
current = current.cause
}
return undefined
}
function readPgErrorField(error: unknown, field: string): string | undefined {
const seen = new Set<unknown>()
let current: unknown = error
while (current !== undefined && current !== null) {
if (seen.has(current)) {
break
}
seen.add(current)
if (typeof current === 'object') {
const value = (current as Record<string, unknown>)[field]
if (typeof value === 'string') {
return value
}
}
if (current instanceof Error && current.cause !== undefined) {
current = current.cause
continue
}
break
}
return undefined
}