errore 0.12.0 → 0.14.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/README.md CHANGED
@@ -26,6 +26,20 @@ console.log(user.username) // user is User, fully narrowed
26
26
  npm install errore
27
27
  ```
28
28
 
29
+ ## Agent Skill
30
+
31
+ errore ships with a skill file that teaches AI coding agents the errore convention. Install it with:
32
+
33
+ ```sh
34
+ npx skills add remorses/errore
35
+ ```
36
+
37
+ Then add this to your `AGENTS.md`:
38
+
39
+ ```
40
+ This codebase uses the errore.org convention. ALWAYS read the errore skill before editing any code.
41
+ ```
42
+
29
43
  ## Quick Start
30
44
 
31
45
  Define typed errors with **variable interpolation** and return **Error or Value** directly:
@@ -46,14 +60,12 @@ class DbError extends errore.createTaggedError({
46
60
 
47
61
  // Function returns Error | Value (no wrapper!)
48
62
  async function getUser(id: string): Promise<NotFoundError | DbError | User> {
49
- const result = await errore.tryAsync({
50
- try: () => db.query(id),
51
- catch: e => new DbError({ reason: e.message, cause: e })
52
- })
53
-
63
+ const result = await db.query(id)
64
+ .catch((e) => new DbError({ reason: e.message, cause: e }))
65
+
54
66
  if (result instanceof Error) return result
55
67
  if (!result) return new NotFoundError({ id })
56
-
68
+
57
69
  return result
58
70
  }
59
71
 
@@ -84,7 +96,7 @@ import * as errore from 'errore'
84
96
  // Base class with shared functionality
85
97
  class AppError extends Error {
86
98
  statusCode: number = 500
87
-
99
+
88
100
  toResponse() {
89
101
  return { error: this.message, code: this.statusCode }
90
102
  }
@@ -105,7 +117,7 @@ class ValidationError extends errore.createTaggedError({
105
117
 
106
118
  class UnauthorizedError extends errore.createTaggedError({
107
119
  name: 'UnauthorizedError',
108
- message: '$message',
120
+
109
121
  extends: AppError
110
122
  }) {}
111
123
 
@@ -118,28 +130,28 @@ async function updateUser(
118
130
  if (!session) {
119
131
  return new UnauthorizedError({ message: 'Not logged in' })
120
132
  }
121
-
133
+
122
134
  const user = await db.users.find(userId)
123
135
  if (!user) {
124
136
  return new NotFoundError({ resource: `User ${userId}` })
125
137
  }
126
-
138
+
127
139
  if (data.email && !isValidEmail(data.email)) {
128
140
  return new ValidationError({ field: 'email', reason: 'Invalid email format' })
129
141
  }
130
-
142
+
131
143
  return db.users.update(userId, data)
132
144
  }
133
145
 
134
146
  // API handler
135
147
  app.post('/users/:id', async (req, res) => {
136
148
  const result = await updateUser(req.params.id, req.body)
137
-
149
+
138
150
  if (result instanceof Error) {
139
151
  // All errors have toResponse() from AppError base
140
152
  return res.status(result.statusCode).json(result.toResponse())
141
153
  }
142
-
154
+
143
155
  return res.json(result)
144
156
  })
145
157
  ```
@@ -172,6 +184,13 @@ class EmptyError extends errore.createTaggedError({
172
184
  }) {}
173
185
  new EmptyError() // no args required
174
186
 
187
+ // Message omitted — caller provides it at construction time
188
+ class GenericError extends errore.createTaggedError({
189
+ name: 'GenericError',
190
+ }) {}
191
+ new GenericError({ message: 'caller decides the message' })
192
+ // fingerprint is stable regardless of what message is passed
193
+
175
194
  // With cause for error chaining
176
195
  class WrapperError extends errore.createTaggedError({
177
196
  name: 'WrapperError',
@@ -195,6 +214,8 @@ err.statusCode // 500 (inherited from AppError)
195
214
  err instanceof AppError // true
196
215
  ```
197
216
 
217
+ **Reserved variable names:** `$_tag`, `$name`, `$stack`, `$cause` cannot be used in message templates — they conflict with Error internals.
218
+
198
219
  ### Error Wrapping and Context
199
220
 
200
221
  Wrap errors with additional context while **preserving the original error** via `cause`:
@@ -203,11 +224,11 @@ Wrap errors with additional context while **preserving the original error** via
203
224
  // Wrap with context, preserve original in cause
204
225
  async function processUser(id: string): Promise<ServiceError | ProcessedUser> {
205
226
  const user = await getUser(id) // returns NotFoundError | User
206
-
227
+
207
228
  if (user instanceof Error) {
208
229
  return new ServiceError({ id, cause: user })
209
230
  }
210
-
231
+
211
232
  return process(user)
212
233
  }
213
234
 
@@ -215,7 +236,7 @@ async function processUser(id: string): Promise<ServiceError | ProcessedUser> {
215
236
  const result = await processUser('123')
216
237
  if (result instanceof Error) {
217
238
  console.log(result.message) // "Failed to process user 123"
218
-
239
+
219
240
  if (result.cause instanceof NotFoundError) {
220
241
  console.log(result.cause.id) // access original error's properties
221
242
  }
@@ -283,9 +304,9 @@ This solves the problem where `result.cause instanceof MyError` only checks one
283
304
 
284
305
  ```ts
285
306
  // A -> B -> C chain
286
- const c = new DbError({ message: 'connection reset' })
307
+ const c = new DbError({ reason: 'connection reset' })
287
308
  const b = new ServiceError({ id: '123', cause: c })
288
- const a = new ApiError({ message: 'request failed', cause: b })
309
+ const a = new NotFoundError({ id: '456', cause: b })
289
310
 
290
311
  // Manual check only finds B
291
312
  a.cause instanceof DbError // false — only checks one level
@@ -355,10 +376,11 @@ const parsed = errore.try({
355
376
  catch: e => new ParseError({ reason: e.message, cause: e })
356
377
  })
357
378
 
358
- // Async
359
- const response = await errore.tryAsync(() => fetch(url))
379
+ // Async — prefer .catch() for promises (no wrapper needed)
380
+ const response = await fetch(url)
381
+ .catch((e) => new NetworkError({ url, cause: e }))
360
382
 
361
- // Async - with custom error
383
+ // Async errore.tryAsync also works, but .catch() is preferred
362
384
  const response = await errore.tryAsync({
363
385
  try: () => fetch(url),
364
386
  catch: e => new NetworkError({ url, cause: e })
@@ -366,6 +388,8 @@ const response = await errore.tryAsync({
366
388
  ```
367
389
 
368
390
  > **Best practices for `try` / `tryAsync`:**
391
+ > - **For async code, prefer `.catch()`** — `promise.catch((e) => new MyError({ cause: e }))` is simpler and avoids the wrapper. `errore.tryAsync` still works but `.catch()` is the idiomatic choice.
392
+ > - **Use `errore.try` for sync code** — there's no equivalent of `.catch()` for synchronous throwing calls, so `errore.try(() => JSON.parse(input))` is the right tool.
369
393
  > - **Use as low as possible in the call stack** — only at boundaries with uncontrolled dependencies (third-party libs, `JSON.parse`, `fetch`, file I/O). Your own functions should return errors as values, never throw.
370
394
  > - **Keep the callback minimal** — wrap only the single throwing call, not your business logic. The `try` callback should be a one-liner.
371
395
  > - **Always prefer `errore.try` over `errore.tryFn`** — they are the same function, but `try` is the canonical name.
@@ -764,7 +788,7 @@ if (user instanceof Error) return user
764
788
  console.log(user.name)
765
789
  ```
766
790
 
767
- The `errore` package just provides conveniences: `createTaggedError` for less boilerplate, `matchError` for exhaustive pattern matching, `tryAsync` for catching exceptions. But the core pattern—**errors as union types**—works with zero dependencies.
791
+ The `errore` package just provides conveniences: `createTaggedError` for less boilerplate, `matchError` for exhaustive pattern matching, `try` for catching sync exceptions (and `.catch()` for async promises). But the core pattern—**errors as union types**—works with zero dependencies.
768
792
 
769
793
  ### Perfect for Libraries
770
794