errore 0.2.0 → 0.4.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 +154 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -125,6 +125,69 @@ const posts = andThen(user, u => fetchPosts(u.id))
|
|
|
125
125
|
const logged = tap(user, u => console.log('Got user:', u.name))
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
+
### Composing Operations
|
|
129
|
+
|
|
130
|
+
Chain multiple operations together:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { map, andThen, mapError, isError } from 'errore'
|
|
134
|
+
|
|
135
|
+
// Define operations that can fail
|
|
136
|
+
function parseNumber(s: string): ValidationError | number { ... }
|
|
137
|
+
function validatePositive(n: number): ValidationError | number { ... }
|
|
138
|
+
function divide(a: number, b: number): DivisionError | number { ... }
|
|
139
|
+
|
|
140
|
+
// Compose with nested calls
|
|
141
|
+
const result = andThen(
|
|
142
|
+
andThen(parseNumber(input), validatePositive),
|
|
143
|
+
n => divide(100, n)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
// Or step by step (often clearer)
|
|
147
|
+
function calculate(input: string): ValidationError | DivisionError | number {
|
|
148
|
+
const parsed = parseNumber(input)
|
|
149
|
+
if (isError(parsed)) return parsed
|
|
150
|
+
|
|
151
|
+
const validated = validatePositive(parsed)
|
|
152
|
+
if (isError(validated)) return validated
|
|
153
|
+
|
|
154
|
+
return divide(100, validated)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Transform errors at the end
|
|
158
|
+
const appResult = mapError(
|
|
159
|
+
calculate(userInput),
|
|
160
|
+
e => new AppError({ source: e._tag, message: e.message })
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Real-world async composition:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
async function processOrder(orderId: string): Promise<OrderError | Receipt> {
|
|
168
|
+
const order = await fetchOrder(orderId)
|
|
169
|
+
if (isError(order)) return order
|
|
170
|
+
|
|
171
|
+
const validated = validateOrder(order)
|
|
172
|
+
if (isError(validated)) return validated
|
|
173
|
+
|
|
174
|
+
const payment = await processPayment(validated)
|
|
175
|
+
if (isError(payment)) return payment
|
|
176
|
+
|
|
177
|
+
return generateReceipt(payment)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Caller gets union of all possible errors
|
|
181
|
+
const receipt = await processOrder('123')
|
|
182
|
+
if (isError(receipt)) {
|
|
183
|
+
matchError(receipt, {
|
|
184
|
+
NotFoundError: e => `Order ${e.id} not found`,
|
|
185
|
+
ValidationError: e => `Invalid: ${e.field}`,
|
|
186
|
+
PaymentError: e => `Payment failed: ${e.reason}`,
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
128
191
|
### Extraction
|
|
129
192
|
|
|
130
193
|
```ts
|
|
@@ -201,6 +264,96 @@ This works because:
|
|
|
201
264
|
2. Custom error classes extend `Error`
|
|
202
265
|
3. After an `instanceof Error` check, TS excludes all Error subtypes
|
|
203
266
|
|
|
267
|
+
## Result + Option Combined: `Error | T | null`
|
|
268
|
+
|
|
269
|
+
One of errore's best features: you can naturally combine error handling with optional values. No wrapper nesting needed!
|
|
270
|
+
|
|
271
|
+
In Rust, you'd need `Result<Option<T>, E>` or `Option<Result<T, E>>` and worry about the order. Here it's just a union:
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
// Result + Option in one natural type
|
|
275
|
+
function findUser(id: string): NotFoundError | User | null {
|
|
276
|
+
if (id === 'bad') return new NotFoundError({ id })
|
|
277
|
+
if (id === 'missing') return null
|
|
278
|
+
return { id, name: 'Alice' }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const user = findUser('123')
|
|
282
|
+
|
|
283
|
+
// Handle error first
|
|
284
|
+
if (isError(user)) {
|
|
285
|
+
return user.message // TypeScript: user is NotFoundError
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Handle null/missing case - use ?. and ?? naturally!
|
|
289
|
+
const name = user?.name ?? 'Anonymous'
|
|
290
|
+
|
|
291
|
+
// Or check explicitly
|
|
292
|
+
if (user === null) {
|
|
293
|
+
return 'User not found'
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// TypeScript knows: user is User
|
|
297
|
+
console.log(user.name)
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Works with `undefined` too
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
function lookup(key: string): NetworkError | string | undefined {
|
|
304
|
+
if (key === 'fail') return new NetworkError({ url: '/api', message: 'Failed' })
|
|
305
|
+
if (key === 'missing') return undefined
|
|
306
|
+
return 'found-value'
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const value = lookup('key')
|
|
310
|
+
|
|
311
|
+
if (isError(value)) return value
|
|
312
|
+
|
|
313
|
+
// ?? works naturally with undefined
|
|
314
|
+
const result = value ?? 'default'
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Triple union: `Error | T | null | undefined`
|
|
318
|
+
|
|
319
|
+
Even this works with full type inference:
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
function query(sql: string): ValidationError | { rows: string[] } | null | undefined {
|
|
323
|
+
if (sql === 'invalid') return new ValidationError({ field: 'sql', message: 'Bad' })
|
|
324
|
+
if (sql === 'empty') return null // explicitly no data
|
|
325
|
+
if (sql === 'no-table') return undefined // table doesn't exist
|
|
326
|
+
return { rows: ['a', 'b'] }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const result = query('SELECT *')
|
|
330
|
+
|
|
331
|
+
if (isError(result)) {
|
|
332
|
+
return result.field // TypeScript: ValidationError
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (result == null) {
|
|
336
|
+
return 'no data' // handles both null and undefined
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// TypeScript: { rows: string[] }
|
|
340
|
+
console.log(result.rows)
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Why this is better than Rust/Zig
|
|
344
|
+
|
|
345
|
+
| Language | Result + Option | Order matters? |
|
|
346
|
+
|----------|-----------------|----------------|
|
|
347
|
+
| Rust | `Result<Option<T>, E>` or `Option<Result<T, E>>` | Yes, must unwrap in order |
|
|
348
|
+
| Zig | `!?T` (error union + optional) | Yes, specific syntax |
|
|
349
|
+
| **errore** | `Error \| T \| null` | **No!** Check in any order |
|
|
350
|
+
|
|
351
|
+
With errore:
|
|
352
|
+
- Use `?.` and `??` naturally
|
|
353
|
+
- Check `isError()` or `=== null` in any order
|
|
354
|
+
- No unwrapping ceremony
|
|
355
|
+
- TypeScript infers everything
|
|
356
|
+
|
|
204
357
|
## Comparison with Result Types
|
|
205
358
|
|
|
206
359
|
| Result Pattern | errore |
|
|
@@ -210,6 +363,7 @@ This works because:
|
|
|
210
363
|
| `result.value` | direct access after guard |
|
|
211
364
|
| `result.map(fn)` | `map(result, fn)` |
|
|
212
365
|
| `Result<User, Error>` | `Error \| User` |
|
|
366
|
+
| `Result<Option<T>, E>` | `Error \| T \| null` |
|
|
213
367
|
|
|
214
368
|
## License
|
|
215
369
|
|