ts-procedures 5.16.0 → 6.0.1

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.
Files changed (147) hide show
  1. package/README.md +2 -0
  2. package/agent_config/claude-code/agents/ts-procedures-architect.md +13 -6
  3. package/agent_config/claude-code/skills/ts-procedures/SKILL.md +26 -4
  4. package/agent_config/claude-code/skills/ts-procedures/anti-patterns.md +87 -19
  5. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +162 -16
  6. package/agent_config/claude-code/skills/ts-procedures/patterns.md +179 -16
  7. package/agent_config/claude-code/skills/ts-procedures-review/SKILL.md +1 -1
  8. package/agent_config/claude-code/skills/ts-procedures-review/checklist.md +20 -12
  9. package/agent_config/claude-code/skills/ts-procedures-scaffold/SKILL.md +2 -1
  10. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/client.md +22 -15
  11. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/express-rpc.md +20 -17
  12. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-api.md +20 -16
  13. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-rpc.md +20 -17
  14. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-stream.md +16 -3
  15. package/agent_config/copilot/copilot-instructions.md +78 -12
  16. package/agent_config/cursor/cursorrules +78 -12
  17. package/build/client/call.d.ts +2 -1
  18. package/build/client/call.js +9 -1
  19. package/build/client/call.js.map +1 -1
  20. package/build/client/error-dispatch.d.ts +13 -0
  21. package/build/client/error-dispatch.js +26 -0
  22. package/build/client/error-dispatch.js.map +1 -0
  23. package/build/client/error-dispatch.test.d.ts +1 -0
  24. package/build/client/error-dispatch.test.js +56 -0
  25. package/build/client/error-dispatch.test.js.map +1 -0
  26. package/build/client/fetch-adapter.js +10 -4
  27. package/build/client/fetch-adapter.js.map +1 -1
  28. package/build/client/index.d.ts +2 -1
  29. package/build/client/index.js +5 -1
  30. package/build/client/index.js.map +1 -1
  31. package/build/client/stream.d.ts +2 -1
  32. package/build/client/stream.js +13 -3
  33. package/build/client/stream.js.map +1 -1
  34. package/build/client/typed-error-dispatch.test.d.ts +1 -0
  35. package/build/client/typed-error-dispatch.test.js +168 -0
  36. package/build/client/typed-error-dispatch.test.js.map +1 -0
  37. package/build/client/types.d.ts +37 -0
  38. package/build/codegen/e2e.test.js +9 -4
  39. package/build/codegen/e2e.test.js.map +1 -1
  40. package/build/codegen/emit-client-runtime.js +4 -0
  41. package/build/codegen/emit-client-runtime.js.map +1 -1
  42. package/build/codegen/emit-errors.d.ts +17 -6
  43. package/build/codegen/emit-errors.integration.test.d.ts +1 -0
  44. package/build/codegen/emit-errors.integration.test.js +162 -0
  45. package/build/codegen/emit-errors.integration.test.js.map +1 -0
  46. package/build/codegen/emit-errors.js +50 -39
  47. package/build/codegen/emit-errors.js.map +1 -1
  48. package/build/codegen/emit-errors.test.js +75 -78
  49. package/build/codegen/emit-errors.test.js.map +1 -1
  50. package/build/codegen/emit-index.d.ts +7 -0
  51. package/build/codegen/emit-index.js +26 -4
  52. package/build/codegen/emit-index.js.map +1 -1
  53. package/build/codegen/emit-index.test.js +55 -23
  54. package/build/codegen/emit-index.test.js.map +1 -1
  55. package/build/codegen/emit-scope.d.ts +8 -0
  56. package/build/codegen/emit-scope.js +82 -7
  57. package/build/codegen/emit-scope.js.map +1 -1
  58. package/build/codegen/pipeline.js +22 -2
  59. package/build/codegen/pipeline.js.map +1 -1
  60. package/build/implementations/http/doc-registry.d.ts +17 -1
  61. package/build/implementations/http/doc-registry.js +47 -79
  62. package/build/implementations/http/doc-registry.js.map +1 -1
  63. package/build/implementations/http/doc-registry.test.js +149 -16
  64. package/build/implementations/http/doc-registry.test.js.map +1 -1
  65. package/build/implementations/http/error-taxonomy.d.ts +249 -0
  66. package/build/implementations/http/error-taxonomy.js +252 -0
  67. package/build/implementations/http/error-taxonomy.js.map +1 -0
  68. package/build/implementations/http/error-taxonomy.test.d.ts +1 -0
  69. package/build/implementations/http/error-taxonomy.test.js +399 -0
  70. package/build/implementations/http/error-taxonomy.test.js.map +1 -0
  71. package/build/implementations/http/express-rpc/error-taxonomy.test.d.ts +1 -0
  72. package/build/implementations/http/express-rpc/error-taxonomy.test.js +83 -0
  73. package/build/implementations/http/express-rpc/error-taxonomy.test.js.map +1 -0
  74. package/build/implementations/http/express-rpc/index.d.ts +39 -8
  75. package/build/implementations/http/express-rpc/index.js +39 -8
  76. package/build/implementations/http/express-rpc/index.js.map +1 -1
  77. package/build/implementations/http/hono-api/error-taxonomy.test.d.ts +1 -0
  78. package/build/implementations/http/hono-api/error-taxonomy.test.js +137 -0
  79. package/build/implementations/http/hono-api/error-taxonomy.test.js.map +1 -0
  80. package/build/implementations/http/hono-api/index.d.ts +38 -1
  81. package/build/implementations/http/hono-api/index.js +32 -0
  82. package/build/implementations/http/hono-api/index.js.map +1 -1
  83. package/build/implementations/http/hono-rpc/error-taxonomy.test.d.ts +1 -0
  84. package/build/implementations/http/hono-rpc/error-taxonomy.test.js +64 -0
  85. package/build/implementations/http/hono-rpc/error-taxonomy.test.js.map +1 -0
  86. package/build/implementations/http/hono-rpc/index.d.ts +34 -7
  87. package/build/implementations/http/hono-rpc/index.js +31 -4
  88. package/build/implementations/http/hono-rpc/index.js.map +1 -1
  89. package/build/implementations/http/hono-stream/error-taxonomy.test.d.ts +1 -0
  90. package/build/implementations/http/hono-stream/error-taxonomy.test.js +87 -0
  91. package/build/implementations/http/hono-stream/error-taxonomy.test.js.map +1 -0
  92. package/build/implementations/http/hono-stream/index.d.ts +40 -3
  93. package/build/implementations/http/hono-stream/index.js +37 -10
  94. package/build/implementations/http/hono-stream/index.js.map +1 -1
  95. package/build/implementations/http/hono-stream/index.test.js +45 -18
  96. package/build/implementations/http/hono-stream/index.test.js.map +1 -1
  97. package/build/implementations/http/on-request-error.test.d.ts +1 -0
  98. package/build/implementations/http/on-request-error.test.js +173 -0
  99. package/build/implementations/http/on-request-error.test.js.map +1 -0
  100. package/build/implementations/http/route-errors.test.d.ts +1 -0
  101. package/build/implementations/http/route-errors.test.js +139 -0
  102. package/build/implementations/http/route-errors.test.js.map +1 -0
  103. package/build/implementations/types.d.ts +43 -3
  104. package/docs/client-and-codegen.md +105 -12
  105. package/docs/core.md +14 -5
  106. package/docs/http-integrations.md +138 -5
  107. package/docs/streaming.md +3 -1
  108. package/docs/superpowers/plans/2026-04-24-doc-registry-simplification.md +886 -0
  109. package/package.json +7 -2
  110. package/src/client/call.ts +10 -1
  111. package/src/client/error-dispatch.test.ts +72 -0
  112. package/src/client/error-dispatch.ts +27 -0
  113. package/src/client/fetch-adapter.ts +11 -5
  114. package/src/client/index.ts +9 -0
  115. package/src/client/stream.ts +14 -3
  116. package/src/client/typed-error-dispatch.test.ts +211 -0
  117. package/src/client/types.ts +42 -0
  118. package/src/codegen/e2e.test.ts +9 -4
  119. package/src/codegen/emit-client-runtime.ts +4 -0
  120. package/src/codegen/emit-errors.integration.test.ts +183 -0
  121. package/src/codegen/emit-errors.test.ts +91 -87
  122. package/src/codegen/emit-errors.ts +123 -41
  123. package/src/codegen/emit-index.test.ts +68 -24
  124. package/src/codegen/emit-index.ts +66 -4
  125. package/src/codegen/emit-scope.ts +124 -7
  126. package/src/codegen/pipeline.ts +25 -2
  127. package/src/implementations/http/README.md +21 -7
  128. package/src/implementations/http/doc-registry.test.ts +164 -16
  129. package/src/implementations/http/doc-registry.ts +58 -82
  130. package/src/implementations/http/error-taxonomy.test.ts +438 -0
  131. package/src/implementations/http/error-taxonomy.ts +361 -0
  132. package/src/implementations/http/express-rpc/README.md +23 -24
  133. package/src/implementations/http/express-rpc/error-taxonomy.test.ts +103 -0
  134. package/src/implementations/http/express-rpc/index.ts +75 -14
  135. package/src/implementations/http/hono-api/README.md +284 -0
  136. package/src/implementations/http/hono-api/error-taxonomy.test.ts +179 -0
  137. package/src/implementations/http/hono-api/index.ts +76 -1
  138. package/src/implementations/http/hono-rpc/README.md +20 -21
  139. package/src/implementations/http/hono-rpc/error-taxonomy.test.ts +82 -0
  140. package/src/implementations/http/hono-rpc/index.ts +65 -9
  141. package/src/implementations/http/hono-stream/README.md +44 -25
  142. package/src/implementations/http/hono-stream/error-taxonomy.test.ts +98 -0
  143. package/src/implementations/http/hono-stream/index.test.ts +54 -18
  144. package/src/implementations/http/hono-stream/index.ts +83 -13
  145. package/src/implementations/http/on-request-error.test.ts +201 -0
  146. package/src/implementations/http/route-errors.test.ts +176 -0
  147. package/src/implementations/types.ts +43 -3
@@ -0,0 +1,252 @@
1
+ import { ProcedureError, ProcedureValidationError, ProcedureYieldValidationError, } from '../../errors.js';
2
+ /**
3
+ * Identity helper that preserves literal inference and:
4
+ * 1. validates each entry has exactly one discriminator (`class` xor `match`);
5
+ * 2. topologically sorts `class:` entries so subclasses precede base classes.
6
+ *
7
+ * The sort is stable — entries unrelated by inheritance keep their declared
8
+ * order. Predicate entries (with `match:`) always keep declared order relative
9
+ * to class entries, so a predicate that's intentionally narrower can be placed
10
+ * before a class entry to take precedence.
11
+ */
12
+ export function defineErrorTaxonomy(entries) {
13
+ const pairs = Object.entries(entries);
14
+ for (const [key, entry] of pairs) {
15
+ const hasClass = entry.class !== undefined;
16
+ const hasMatch = entry.match !== undefined;
17
+ if (hasClass === hasMatch) {
18
+ throw new Error(`Error taxonomy entry "${key}" must define exactly one of { class, match }.`);
19
+ }
20
+ }
21
+ // Stable sort: subclass-of-the-other → -1, other-subclass-of-this → 1, else 0.
22
+ pairs.sort(([, a], [, b]) => {
23
+ if (!a.class || !b.class)
24
+ return 0;
25
+ if (a.class === b.class)
26
+ return 0;
27
+ if (a.class.prototype instanceof b.class)
28
+ return -1;
29
+ if (b.class.prototype instanceof a.class)
30
+ return 1;
31
+ return 0;
32
+ });
33
+ return Object.fromEntries(pairs);
34
+ }
35
+ /**
36
+ * Default taxonomy covering framework error classes that can be thrown by a
37
+ * handler at request time. Layered after the user taxonomy during resolution.
38
+ *
39
+ * `ProcedureError` uses `match:` rather than `class:` so it matches only
40
+ * direct throws (e.g. from `ctx.error()`). When the core wraps a non-ProcedureError
41
+ * into a ProcedureError with `cause`, that wrapper falls through — the
42
+ * resolver unwraps the cause and the user taxonomy / `unknownError` sees the
43
+ * real thrown value.
44
+ */
45
+ export const defaultErrorTaxonomy = defineErrorTaxonomy({
46
+ ProcedureValidationError: {
47
+ class: ProcedureValidationError,
48
+ statusCode: 400,
49
+ description: 'Schema validation failed for the procedure input parameters.',
50
+ toResponse: (err) => ({
51
+ name: 'ProcedureValidationError',
52
+ procedureName: err.procedureName,
53
+ message: err.message,
54
+ errors: err.errors,
55
+ }),
56
+ schema: {
57
+ type: 'object',
58
+ properties: {
59
+ name: { type: 'string', const: 'ProcedureValidationError' },
60
+ procedureName: { type: 'string' },
61
+ message: { type: 'string' },
62
+ errors: {
63
+ type: 'array',
64
+ items: {
65
+ type: 'object',
66
+ properties: {
67
+ instancePath: { type: 'string' },
68
+ message: { type: 'string' },
69
+ },
70
+ },
71
+ },
72
+ },
73
+ required: ['name', 'procedureName', 'message'],
74
+ },
75
+ },
76
+ ProcedureYieldValidationError: {
77
+ class: ProcedureYieldValidationError,
78
+ statusCode: 500,
79
+ description: 'Schema validation failed for a yielded value in a streaming procedure.',
80
+ toResponse: (err) => ({
81
+ name: 'ProcedureYieldValidationError',
82
+ procedureName: err.procedureName,
83
+ message: err.message,
84
+ errors: err.errors,
85
+ }),
86
+ schema: {
87
+ type: 'object',
88
+ properties: {
89
+ name: { type: 'string', const: 'ProcedureYieldValidationError' },
90
+ procedureName: { type: 'string' },
91
+ message: { type: 'string' },
92
+ errors: {
93
+ type: 'array',
94
+ items: {
95
+ type: 'object',
96
+ properties: {
97
+ instancePath: { type: 'string' },
98
+ message: { type: 'string' },
99
+ },
100
+ },
101
+ },
102
+ },
103
+ required: ['name', 'procedureName', 'message'],
104
+ },
105
+ },
106
+ ProcedureError: {
107
+ match: (err) => err instanceof ProcedureError && err.cause === undefined,
108
+ statusCode: 500,
109
+ description: 'An error thrown from within a procedure handler via ctx.error().',
110
+ toResponse: (err) => ({
111
+ name: 'ProcedureError',
112
+ procedureName: err.procedureName,
113
+ message: err.message,
114
+ meta: err.meta,
115
+ }),
116
+ schema: {
117
+ type: 'object',
118
+ properties: {
119
+ name: { type: 'string', const: 'ProcedureError' },
120
+ procedureName: { type: 'string' },
121
+ message: { type: 'string' },
122
+ meta: { type: 'object' },
123
+ },
124
+ required: ['name', 'procedureName', 'message'],
125
+ },
126
+ },
127
+ });
128
+ /**
129
+ * Doc-only entry for `ProcedureRegistrationError`, which is thrown at
130
+ * procedure-definition time (never at request time) and therefore doesn't
131
+ * appear in the runtime taxonomy. Consumers still see it in the error catalog
132
+ * via `DocRegistry.defaultErrors()`.
133
+ */
134
+ export const PROCEDURE_REGISTRATION_ERROR_DOC = {
135
+ name: 'ProcedureRegistrationError',
136
+ statusCode: 500,
137
+ description: 'An invalid schema or configuration was detected at procedure registration time.',
138
+ schema: {
139
+ type: 'object',
140
+ properties: {
141
+ name: { type: 'string', const: 'ProcedureRegistrationError' },
142
+ procedureName: { type: 'string' },
143
+ message: { type: 'string' },
144
+ },
145
+ required: ['name', 'procedureName', 'message'],
146
+ },
147
+ };
148
+ /**
149
+ * Converts a taxonomy into {@link ErrorDoc} objects suitable for a DocEnvelope.
150
+ *
151
+ * @internal Used by `DocRegistry` to merge taxonomy entries into the envelope.
152
+ * Consumers should pass their taxonomy directly to `new DocRegistry({ errors: taxonomy })`
153
+ * rather than calling this helper — the constructor handles the conversion.
154
+ */
155
+ export function taxonomyToErrorDocs(taxonomy) {
156
+ return Object.entries(taxonomy).map(([key, entry]) => ({
157
+ name: key,
158
+ statusCode: entry.statusCode,
159
+ description: entry.description ?? '',
160
+ schema: entry.schema,
161
+ }));
162
+ }
163
+ /**
164
+ * Injects `{ name: key }` when `rawBody` is an object without a `name` field.
165
+ * Guarantees wire-protocol consistency (client dispatchers discriminate on
166
+ * `body.name`) without forcing every `toResponse` to repeat it.
167
+ */
168
+ function ensureName(rawBody, key) {
169
+ if (rawBody && typeof rawBody === 'object' && !('name' in rawBody)) {
170
+ return { name: key, ...rawBody };
171
+ }
172
+ return rawBody;
173
+ }
174
+ /**
175
+ * Matches a thrown value against the user taxonomy, then (if `includeDefaults`)
176
+ * the default taxonomy, then the `unknownError` config. Returns `null` when
177
+ * nothing matches — callers fall through to their builder's imperative
178
+ * `onError` callback and the hard default.
179
+ *
180
+ * The core wraps any non-ProcedureError thrown by a handler into a
181
+ * ProcedureError with `cause` set to the original. This resolver unwraps that:
182
+ * candidates are checked in the order `[cause, outer]` so a user taxonomy sees
183
+ * its own error classes rather than the wrapper. The default `ProcedureError`
184
+ * entry uses a `match:` predicate that excludes wrappers so they reach
185
+ * `unknownError` instead.
186
+ *
187
+ * Side effects (`onCatch`) are deferred into `runOnCatch` so the caller decides
188
+ * when to execute them relative to writing the response.
189
+ */
190
+ export function resolveErrorResponse(params) {
191
+ const { err, userTaxonomy, includeDefaults = true, unknownError, procedure, raw, } = params;
192
+ const wrappedCause = err instanceof ProcedureError && err.cause !== undefined
193
+ ? err.cause
194
+ : undefined;
195
+ // Most-specific candidate first so a matching user entry on `cause` wins over
196
+ // a matching entry on the outer wrapper.
197
+ const candidates = wrappedCause !== undefined ? [wrappedCause, err] : [err];
198
+ const tryMatch = (tax) => {
199
+ for (const [key, entry] of Object.entries(tax)) {
200
+ for (const candidate of candidates) {
201
+ const matched = entry.class
202
+ ? candidate instanceof entry.class
203
+ : entry.match
204
+ ? entry.match(candidate)
205
+ : false;
206
+ if (!matched)
207
+ continue;
208
+ const rawBody = entry.toResponse
209
+ ? entry.toResponse(candidate, { key })
210
+ : {
211
+ name: key,
212
+ message: candidate instanceof Error ? candidate.message : String(candidate),
213
+ };
214
+ const body = ensureName(rawBody, key);
215
+ return {
216
+ statusCode: entry.statusCode,
217
+ body,
218
+ runOnCatch: async () => {
219
+ if (entry.onCatch) {
220
+ await entry.onCatch(candidate, { procedure, key, raw });
221
+ }
222
+ },
223
+ };
224
+ }
225
+ }
226
+ return null;
227
+ };
228
+ if (userTaxonomy) {
229
+ const hit = tryMatch(userTaxonomy);
230
+ if (hit)
231
+ return hit;
232
+ }
233
+ if (includeDefaults) {
234
+ const hit = tryMatch(defaultErrorTaxonomy);
235
+ if (hit)
236
+ return hit;
237
+ }
238
+ if (unknownError) {
239
+ const mostSpecific = candidates[0];
240
+ return {
241
+ statusCode: unknownError.statusCode ?? 500,
242
+ body: unknownError.toResponse(mostSpecific),
243
+ runOnCatch: async () => {
244
+ if (unknownError.onCatch) {
245
+ await unknownError.onCatch(mostSpecific, { procedure, raw });
246
+ }
247
+ },
248
+ };
249
+ }
250
+ return null;
251
+ }
252
+ //# sourceMappingURL=error-taxonomy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-taxonomy.js","sourceRoot":"","sources":["../../../src/implementations/http/error-taxonomy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,399 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { ProcedureError, ProcedureValidationError, ProcedureYieldValidationError, } from '../../errors.js';
3
+ import { defineErrorTaxonomy, resolveErrorResponse, defaultErrorTaxonomy, } from './error-taxonomy.js';
4
+ class UseCaseError extends Error {
5
+ externalMsg;
6
+ internalMsg;
7
+ constructor(externalMsg, internalMsg) {
8
+ super(externalMsg);
9
+ this.externalMsg = externalMsg;
10
+ this.internalMsg = internalMsg;
11
+ this.name = 'UseCaseError';
12
+ Object.setPrototypeOf(this, UseCaseError.prototype);
13
+ }
14
+ }
15
+ class AuthError extends Error {
16
+ reason;
17
+ constructor(reason) {
18
+ super(reason);
19
+ this.reason = reason;
20
+ this.name = 'AuthError';
21
+ Object.setPrototypeOf(this, AuthError.prototype);
22
+ }
23
+ }
24
+ const fakeProcedure = { name: 'Test', config: {}, handler: async () => { } };
25
+ describe('defineErrorTaxonomy', () => {
26
+ test('validates exactly one discriminator per entry', () => {
27
+ expect(() => defineErrorTaxonomy({
28
+ Bad: { statusCode: 400 },
29
+ })).toThrow(/exactly one of/);
30
+ expect(() => defineErrorTaxonomy({
31
+ Bad: {
32
+ class: Error,
33
+ match: (e) => e instanceof Error,
34
+ statusCode: 400,
35
+ },
36
+ })).toThrow(/exactly one of/);
37
+ });
38
+ test('accepts a valid entry', () => {
39
+ const t = defineErrorTaxonomy({
40
+ UseCaseError: { class: UseCaseError, statusCode: 422 },
41
+ });
42
+ expect(t.UseCaseError.statusCode).toBe(422);
43
+ });
44
+ });
45
+ describe('resolveErrorResponse', () => {
46
+ test('class match uses toResponse output', () => {
47
+ const taxonomy = defineErrorTaxonomy({
48
+ UseCaseError: {
49
+ class: UseCaseError,
50
+ statusCode: 422,
51
+ toResponse: (err) => ({ name: 'UseCaseError', message: err.externalMsg }),
52
+ },
53
+ });
54
+ const resolved = resolveErrorResponse({
55
+ err: new UseCaseError('external', 'internal'),
56
+ userTaxonomy: taxonomy,
57
+ procedure: fakeProcedure,
58
+ raw: {},
59
+ });
60
+ expect(resolved?.statusCode).toBe(422);
61
+ expect(resolved?.body).toEqual({ name: 'UseCaseError', message: 'external' });
62
+ });
63
+ test('match predicate catches 3rd-party errors', () => {
64
+ const mongoLike = Object.assign(new Error('dup'), { name: 'MongoServerError', code: 11000 });
65
+ const taxonomy = defineErrorTaxonomy({
66
+ MongoDuplicateKey: {
67
+ match: (e) => e instanceof Error && e.code === 11000,
68
+ statusCode: 409,
69
+ toResponse: () => ({ name: 'Conflict' }),
70
+ },
71
+ });
72
+ const resolved = resolveErrorResponse({
73
+ err: mongoLike,
74
+ userTaxonomy: taxonomy,
75
+ procedure: fakeProcedure,
76
+ raw: {},
77
+ });
78
+ expect(resolved?.statusCode).toBe(409);
79
+ expect(resolved?.body).toEqual({ name: 'Conflict' });
80
+ });
81
+ test('default toResponse emits { name, message } from entry key', () => {
82
+ const taxonomy = defineErrorTaxonomy({
83
+ AuthError: { class: AuthError, statusCode: 401 },
84
+ });
85
+ const resolved = resolveErrorResponse({
86
+ err: new AuthError('unauthenticated'),
87
+ userTaxonomy: taxonomy,
88
+ procedure: fakeProcedure,
89
+ raw: {},
90
+ });
91
+ expect(resolved?.body).toEqual({ name: 'AuthError', message: 'unauthenticated' });
92
+ });
93
+ test('first matching entry wins — subclass declared before base', () => {
94
+ const taxonomy = defineErrorTaxonomy({
95
+ ProcedureValidationError: {
96
+ class: ProcedureValidationError,
97
+ statusCode: 400,
98
+ toResponse: () => ({ kind: 'validation' }),
99
+ },
100
+ ProcedureError: {
101
+ class: ProcedureError,
102
+ statusCode: 500,
103
+ toResponse: () => ({ kind: 'base' }),
104
+ },
105
+ });
106
+ const resolved = resolveErrorResponse({
107
+ err: new ProcedureValidationError('Test', 'bad', []),
108
+ userTaxonomy: taxonomy,
109
+ procedure: fakeProcedure,
110
+ includeDefaults: false,
111
+ raw: {},
112
+ });
113
+ expect(resolved?.statusCode).toBe(400);
114
+ expect(resolved?.body).toEqual({ name: 'ProcedureValidationError', kind: 'validation' });
115
+ });
116
+ test('topological sort fixes a subclass that was declared after its base', () => {
117
+ // Under first-match-wins without sorting, the base would catch the
118
+ // subclass. defineErrorTaxonomy reorders to keep the subclass entry first.
119
+ const taxonomy = defineErrorTaxonomy({
120
+ ProcedureError: {
121
+ class: ProcedureError,
122
+ statusCode: 500,
123
+ toResponse: () => ({ kind: 'base' }),
124
+ },
125
+ ProcedureValidationError: {
126
+ class: ProcedureValidationError,
127
+ statusCode: 400,
128
+ toResponse: () => ({ kind: 'validation' }),
129
+ },
130
+ });
131
+ const resolved = resolveErrorResponse({
132
+ err: new ProcedureValidationError('Test', 'bad', []),
133
+ userTaxonomy: taxonomy,
134
+ procedure: fakeProcedure,
135
+ includeDefaults: false,
136
+ raw: {},
137
+ });
138
+ expect(resolved?.statusCode).toBe(400);
139
+ expect(resolved?.body).toEqual({ name: 'ProcedureValidationError', kind: 'validation' });
140
+ });
141
+ test('falls through to unknownError when nothing matches', () => {
142
+ const resolved = resolveErrorResponse({
143
+ err: new Error('boom'),
144
+ userTaxonomy: defineErrorTaxonomy({
145
+ AuthError: { class: AuthError, statusCode: 401 },
146
+ }),
147
+ includeDefaults: false,
148
+ unknownError: {
149
+ statusCode: 500,
150
+ toResponse: () => ({ name: 'InternalServerError' }),
151
+ },
152
+ procedure: fakeProcedure,
153
+ raw: {},
154
+ });
155
+ expect(resolved?.statusCode).toBe(500);
156
+ expect(resolved?.body).toEqual({ name: 'InternalServerError' });
157
+ });
158
+ test('returns null when nothing matches and no unknownError', () => {
159
+ const resolved = resolveErrorResponse({
160
+ err: new Error('boom'),
161
+ userTaxonomy: defineErrorTaxonomy({
162
+ AuthError: { class: AuthError, statusCode: 401 },
163
+ }),
164
+ includeDefaults: false,
165
+ procedure: fakeProcedure,
166
+ raw: {},
167
+ });
168
+ expect(resolved).toBeNull();
169
+ });
170
+ test('default taxonomy catches ProcedureValidationError with status 400', () => {
171
+ const resolved = resolveErrorResponse({
172
+ err: new ProcedureValidationError('Test', 'bad', []),
173
+ procedure: fakeProcedure,
174
+ raw: {},
175
+ });
176
+ expect(resolved?.statusCode).toBe(400);
177
+ expect((resolved?.body).name).toBe('ProcedureValidationError');
178
+ });
179
+ test('default taxonomy catches ProcedureYieldValidationError with status 500', () => {
180
+ const resolved = resolveErrorResponse({
181
+ err: new ProcedureYieldValidationError('Test', 'bad yield', []),
182
+ procedure: fakeProcedure,
183
+ raw: {},
184
+ });
185
+ expect(resolved?.statusCode).toBe(500);
186
+ expect((resolved?.body).name).toBe('ProcedureYieldValidationError');
187
+ });
188
+ test('includeDefaults: false disables the default taxonomy', () => {
189
+ const resolved = resolveErrorResponse({
190
+ err: new ProcedureValidationError('Test', 'bad', []),
191
+ includeDefaults: false,
192
+ procedure: fakeProcedure,
193
+ raw: {},
194
+ });
195
+ expect(resolved).toBeNull();
196
+ });
197
+ test('user entry overrides default for the same class', () => {
198
+ const resolved = resolveErrorResponse({
199
+ err: new ProcedureValidationError('Test', 'bad', []),
200
+ userTaxonomy: defineErrorTaxonomy({
201
+ ProcedureValidationError: {
202
+ class: ProcedureValidationError,
203
+ statusCode: 418,
204
+ toResponse: () => ({ overridden: true }),
205
+ },
206
+ }),
207
+ procedure: fakeProcedure,
208
+ raw: {},
209
+ });
210
+ expect(resolved?.statusCode).toBe(418);
211
+ expect(resolved?.body).toEqual({ name: 'ProcedureValidationError', overridden: true });
212
+ });
213
+ test('onCatch is awaited via runOnCatch', async () => {
214
+ const calls = [];
215
+ const taxonomy = defineErrorTaxonomy({
216
+ UseCaseError: {
217
+ class: UseCaseError,
218
+ statusCode: 422,
219
+ onCatch: async (err) => {
220
+ await new Promise((r) => setTimeout(r, 5));
221
+ calls.push(err.internalMsg);
222
+ },
223
+ },
224
+ });
225
+ const resolved = resolveErrorResponse({
226
+ err: new UseCaseError('ext', 'internal-log'),
227
+ userTaxonomy: taxonomy,
228
+ procedure: fakeProcedure,
229
+ raw: {},
230
+ });
231
+ expect(calls).toEqual([]);
232
+ await resolved.runOnCatch();
233
+ expect(calls).toEqual(['internal-log']);
234
+ });
235
+ test('unknownError onCatch is awaited', async () => {
236
+ const calls = [];
237
+ const resolved = resolveErrorResponse({
238
+ err: new Error('boom'),
239
+ includeDefaults: false,
240
+ unknownError: {
241
+ toResponse: () => ({}),
242
+ onCatch: async (err) => {
243
+ await Promise.resolve();
244
+ calls.push(err);
245
+ },
246
+ },
247
+ procedure: fakeProcedure,
248
+ raw: {},
249
+ });
250
+ await resolved.runOnCatch();
251
+ expect(calls).toHaveLength(1);
252
+ expect(calls[0].message).toBe('boom');
253
+ });
254
+ test('onCatch receives procedure, key and raw', async () => {
255
+ let received;
256
+ const taxonomy = defineErrorTaxonomy({
257
+ AuthError: {
258
+ class: AuthError,
259
+ statusCode: 401,
260
+ onCatch: (_err, ctx) => {
261
+ received = ctx;
262
+ },
263
+ },
264
+ });
265
+ const resolved = resolveErrorResponse({
266
+ err: new AuthError('forbidden'),
267
+ userTaxonomy: taxonomy,
268
+ procedure: fakeProcedure,
269
+ raw: { marker: 'hono-context' },
270
+ });
271
+ await resolved.runOnCatch();
272
+ expect(received.procedure).toBe(fakeProcedure);
273
+ expect(received.key).toBe('AuthError');
274
+ expect(received.raw).toEqual({ marker: 'hono-context' });
275
+ });
276
+ test('defaultErrorTaxonomy exposes all four framework error mappings', () => {
277
+ expect(defaultErrorTaxonomy.ProcedureValidationError.statusCode).toBe(400);
278
+ expect(defaultErrorTaxonomy.ProcedureYieldValidationError.statusCode).toBe(500);
279
+ expect(defaultErrorTaxonomy.ProcedureError.statusCode).toBe(500);
280
+ });
281
+ test('user taxonomy matches cause inside a ProcedureError wrapper', () => {
282
+ // Simulates what the core does when a non-ProcedureError is thrown: wraps
283
+ // into ProcedureError with `cause` preserved.
284
+ const original = new UseCaseError('public', 'private');
285
+ const wrapped = new ProcedureError('Test', 'wrapped');
286
+ wrapped.cause = original;
287
+ const taxonomy = defineErrorTaxonomy({
288
+ UseCaseError: {
289
+ class: UseCaseError,
290
+ statusCode: 422,
291
+ toResponse: (err) => ({ name: 'UseCaseError', message: err.externalMsg }),
292
+ },
293
+ });
294
+ const resolved = resolveErrorResponse({
295
+ err: wrapped,
296
+ userTaxonomy: taxonomy,
297
+ procedure: fakeProcedure,
298
+ raw: {},
299
+ });
300
+ expect(resolved?.statusCode).toBe(422);
301
+ expect(resolved?.body).toEqual({ name: 'UseCaseError', message: 'public' });
302
+ });
303
+ test('wrapped ProcedureError falls through default taxonomy to unknownError', () => {
304
+ const original = new TypeError('db-broke');
305
+ const wrapped = new ProcedureError('Test', 'wrapped');
306
+ wrapped.cause = original;
307
+ const resolved = resolveErrorResponse({
308
+ err: wrapped,
309
+ unknownError: {
310
+ statusCode: 500,
311
+ toResponse: (err) => ({ name: 'InternalServerError', type: err.constructor.name }),
312
+ },
313
+ procedure: fakeProcedure,
314
+ raw: {},
315
+ });
316
+ expect(resolved?.statusCode).toBe(500);
317
+ expect(resolved?.body).toEqual({ name: 'InternalServerError', type: 'TypeError' });
318
+ });
319
+ test('direct ProcedureError (no cause) still caught by default entry', () => {
320
+ const direct = new ProcedureError('Test', 'from ctx.error');
321
+ const resolved = resolveErrorResponse({
322
+ err: direct,
323
+ procedure: fakeProcedure,
324
+ raw: {},
325
+ });
326
+ expect(resolved?.statusCode).toBe(500);
327
+ expect((resolved?.body).name).toBe('ProcedureError');
328
+ });
329
+ test('auto-injects name when toResponse omits it', () => {
330
+ const taxonomy = defineErrorTaxonomy({
331
+ UseCaseError: {
332
+ class: UseCaseError,
333
+ statusCode: 422,
334
+ // Returns a body without `name` — resolver should add one.
335
+ toResponse: (err) => ({ message: err.externalMsg, detail: err.internalMsg }),
336
+ },
337
+ });
338
+ const resolved = resolveErrorResponse({
339
+ err: new UseCaseError('ext', 'int'),
340
+ userTaxonomy: taxonomy,
341
+ procedure: fakeProcedure,
342
+ raw: {},
343
+ });
344
+ expect(resolved?.body).toEqual({ name: 'UseCaseError', message: 'ext', detail: 'int' });
345
+ });
346
+ test('preserves explicit name in toResponse output', () => {
347
+ const taxonomy = defineErrorTaxonomy({
348
+ UseCaseError: {
349
+ class: UseCaseError,
350
+ statusCode: 422,
351
+ toResponse: () => ({ name: 'CustomAlias', reason: 'x' }),
352
+ },
353
+ });
354
+ const resolved = resolveErrorResponse({
355
+ err: new UseCaseError('ext', 'int'),
356
+ userTaxonomy: taxonomy,
357
+ procedure: fakeProcedure,
358
+ raw: {},
359
+ });
360
+ expect((resolved?.body).name).toBe('CustomAlias');
361
+ });
362
+ test('defineErrorTaxonomy topologically sorts class entries (subclass before base)', () => {
363
+ // Declared base-before-subclass on purpose — sort must swap them.
364
+ const taxonomy = defineErrorTaxonomy({
365
+ ProcedureError: {
366
+ class: ProcedureError,
367
+ statusCode: 500,
368
+ toResponse: () => ({ kind: 'base' }),
369
+ },
370
+ ProcedureValidationError: {
371
+ class: ProcedureValidationError,
372
+ statusCode: 400,
373
+ toResponse: () => ({ kind: 'validation' }),
374
+ },
375
+ });
376
+ const keys = Object.keys(taxonomy);
377
+ expect(keys).toEqual(['ProcedureValidationError', 'ProcedureError']);
378
+ const resolved = resolveErrorResponse({
379
+ err: new ProcedureValidationError('Test', 'bad', []),
380
+ userTaxonomy: taxonomy,
381
+ includeDefaults: false,
382
+ procedure: fakeProcedure,
383
+ raw: {},
384
+ });
385
+ expect(resolved?.statusCode).toBe(400);
386
+ });
387
+ test('topological sort preserves declared order for unrelated classes', () => {
388
+ class A extends Error {
389
+ }
390
+ class B extends Error {
391
+ }
392
+ const taxonomy = defineErrorTaxonomy({
393
+ B: { class: B, statusCode: 400 },
394
+ A: { class: A, statusCode: 400 },
395
+ });
396
+ expect(Object.keys(taxonomy)).toEqual(['B', 'A']);
397
+ });
398
+ });
399
+ //# sourceMappingURL=error-taxonomy.test.js.map