# error — Functional Error Handling > `import { catchErrors, wrapErrors, wrapErrorsFn, wrapCustomErrors, mergeErrors, getErrorMetadata, required } from 'puffy-core/error'` > CJS: `const { error: { catchErrors, wrapErrors, wrapErrorsFn, wrapCustomErrors, mergeErrors, getErrorMetadata, required } } = require('puffy-core')` > Also available in snake_case: `catch_errors`, `wrap_errors`, `wrap_errors_fn`, `wrap_custom_errors`, `merge_errors`, `get_error_metadata` This module uses a functional approach to error handling. Instead of throwing and catching exceptions, errors are accumulated in `[errors, data]` tuples called PuffyResponse. Every function that can fail returns this tuple. It is up to the engineer to check for errors and decide what to do. --- ## PuffyResponse PuffyResponse is a custom Array subclass. It always contains exactly 2 elements: `[errors, data]`. - `errors`: Array of Error objects when something failed, or `null` when successful - `data`: The return value on success, or `undefined`/`null` on failure ALWAYS destructure it: ```js // CORRECT — destructure into [errors, data] const [errors, data] = catchErrors(() => doSomething()) if (errors) { console.error(errors[0].message) } else { console.log(data) } // WRONG — do not use as a single object const result = catchErrors(() => doSomething()) console.log(result.data) // WRONG — PuffyResponse has no .data property console.log(result[1]) // Works but unclear — always destructure instead ``` GOTCHA: PuffyResponse is NOT a plain array. `chainAsync` and `chainSync` from `puffy-core/func` use `instanceof PuffyResponse` to detect error tuples. If you manually return `[errors, data]` (a plain array), those functions will NOT treat it as an error response. --- ## catchErrors Universal error wrapper. Wraps sync functions, async functions, and promises. Returns a PuffyResponse `[errors, data]`. ### Signatures ``` catchErrors(fn) → PuffyResponse [errors, data] catchErrors(asyncFn) → Promise catchErrors(promise) → Promise catchErrors(message, fn) → PuffyResponse [errors, data] catchErrors(message, asyncFn) → Promise catchErrors(message, promise) → Promise ``` Parameters: - `message` (string, optional): Context message prepended to the error stack. ALWAYS provide this for clarity. - `fn`: Synchronous function to execute - `asyncFn`: Async function to execute - `promise`: A Promise to await Returns: - On success: `[null, returnValue]` - On failure: `[Array, undefined]` ### Example 1 — Synchronous function (no await needed) ```js const [errors, data] = catchErrors(() => { return 'hello' }) // errors = null // data = 'hello' const [errors, data] = catchErrors(() => { throw new Error('Boom') }) // errors = [Error('Boom')] // data = undefined ``` ### Example 2 — Async function (MUST await) ```js // CORRECT — await the result const [errors, data] = await catchErrors(async () => { const result = await fetch('https://api.example.com/data') return result.json() }) // WRONG — forgetting await returns a Promise, not [errors, data] const [errors, data] = catchErrors(async () => { ... }) // errors is a Promise object, NOT an error array! // This is the #1 most common mistake. ``` ### Example 3 — Wrapping a bare Promise ```js const myPromise = fetch('https://api.example.com/data') const [errors, data] = await catchErrors(myPromise) if (errors) { console.error('Fetch failed:', errors[0].message) } else { console.log('Got:', data) } ``` ### Example 4 — With context message (RECOMMENDED) The context message is prepended to the error stack for traceability: ```js const [errors, data] = await catchErrors( 'Failed to load user profile', async () => { const resp = await fetch(`/api/users/${id}`) if (!resp.ok) throw new Error(`HTTP ${resp.status}`) return resp.json() } ) // On failure: // errors = [ // Error('Failed to load user profile'), ← context (you added this) // Error('HTTP 404') ← original cause // ] ``` ### Example 5 — Nested catchErrors with error propagation This is the canonical pattern for composing multiple operations: ```js const { error: { catchErrors, wrapErrors } } = require('puffy-core') // Or: import { catchErrors, wrapErrors } from 'puffy-core/error' const loadUser = async (id) => { const [errors, data] = await catchErrors( 'Failed to load user', async () => { const [fetchErrors, resp] = await catchErrors( `Failed to fetch user ${id}`, fetch(`/api/users/${id}`) ) if (fetchErrors) throw wrapErrors('Fetch step failed', fetchErrors) const [parseErrors, user] = await catchErrors( 'Failed to parse response', resp.json() ) if (parseErrors) throw wrapErrors('Parse step failed', parseErrors) return user } ) return [errors, data] } // Usage: const [errors, user] = await loadUser(123) if (errors) { // errors is an array from outermost context to innermost cause: // [Error('Failed to load user'), Error('Fetch step failed'), Error('Failed to fetch user 123'), Error('Network error')] console.error(errors.map(e => e.message).join(' > ')) } else { console.log(user.name) } ``` ### Example 6 — Using wrapErrorsFn shorthand (aliased as `e`) ```js const { error: { catchErrors, wrapErrorsFn } } = require('puffy-core') const e = wrapErrorsFn('loadUser failed') const loadUser = async (id) => { const [errors, data] = await catchErrors( 'Failed in loadUser', async () => { const [fetchErr, resp] = await catchErrors( 'Fetch failed', fetch(`/api/users/${id}`) ) if (fetchErr) throw e(fetchErr) return resp.json() } ) return [errors, data] } ``` ### CRITICAL: How to detect async catchErrors detects async functions by checking `fn.constructor.name === 'AsyncFunction'`. This means: - `catchErrors(async () => {...})` — detected as async, returns Promise - `catchErrors(() => somePromise)` — detected as SYNC, returns PuffyResponse immediately (the promise inside is NOT awaited!) - `catchErrors(somePromise)` — detected as Promise, returns Promise ```js // WRONG — sync wrapper around a promise. catchErrors returns immediately, // the inner promise is not awaited. const [errors, data] = catchErrors(() => fetch('/api/data')) // data = Promise object (not the fetch result!) // CORRECT — pass the promise directly const [errors, data] = await catchErrors(fetch('/api/data')) // CORRECT — use async arrow const [errors, data] = await catchErrors(async () => { const resp = await fetch('/api/data') return resp.json() }) ``` --- ## wrapErrors Creates a new Error with nested child errors for richer stack traces. Use this to re-throw caught errors with added context. ### Signature ``` wrapErrors(message: string, ...errorsOrStrings) → Error wrapErrors(message: string, errorsArray, { merge: true }) → Error ``` Parameters: - `message` (string): Context message for this error level - `...errorsOrStrings`: Any mix of strings, Error objects, and arrays of Errors Returns: Error object with: - `.message`: The context message - `.errors`: Array of all child errors - `.stack`: Stack trace - `.response(data)`: Method to attach partial-success data (see below) ### Example — Basic wrapping ```js const [innerErrors, data] = await catchErrors(riskyOperation()) if (innerErrors) { throw wrapErrors('Operation failed', innerErrors) } ``` ### Example — Mixed inputs ```js throw wrapErrors( 'Multiple things went wrong', new Error('First problem'), 'Second problem as string', [new Error('Third'), new Error('Fourth')] ) ``` ### Example — Merge mode With `{ merge: true }`, all error messages are concatenated into a single Error instead of nesting: ```js const errs = [new Error('A'), new Error('B')] // Default — nested (3 errors in stack) const wrapped = wrapErrors('Context', errs) // wrapped.message = 'Context' // wrapped.errors = [Error('A'), Error('B')] // Merge mode — single flat error const merged = wrapErrors('Context', errs, { merge: true }) // merged.message = 'Context\nA\nB' // merged.errors = undefined ``` --- ## wrapErrorsFn Returns a reusable error wrapper function. Sugar for `(...errors) => wrapErrors(message, ...errors)`. ### Signature ``` wrapErrorsFn(message: string) → (...errors) => Error ``` ### Example ```js const e = wrapErrorsFn('loadUser failed') // These are equivalent: throw e(innerErrors) throw wrapErrors('loadUser failed', innerErrors) // Useful when you have multiple error-check points in one function: const processOrder = async (order) => { const e = wrapErrorsFn('processOrder failed') const [err1, user] = await catchErrors('load user', loadUser(order.userId)) if (err1) throw e(err1) const [err2, payment] = await catchErrors('charge payment', charge(user, order.total)) if (err2) throw e(err2) const [err3, confirmation] = await catchErrors('send confirmation', sendEmail(user.email)) if (err3) throw e(err3) return confirmation } ``` --- ## wrapCustomErrors Creates an error wrapper that attaches metadata to errors. Useful for programmatic error inspection (e.g., HTTP status codes). ### Signature ``` wrapCustomErrors(metadata: Object|string) → (message: string, ...errors) => Error ``` Returns: A function that works like wrapErrors but attaches `.metadata` to the error. ### Example ```js const notFound = wrapCustomErrors({ code: 'NOT_FOUND', status: 404 }) const unauthorized = wrapCustomErrors({ code: 'UNAUTHORIZED', status: 401 }) const loadUser = async (id) => { const [errors, user] = await catchErrors( 'Failed to load user', async () => { const resp = await fetch(`/api/users/${id}`) if (resp.status === 404) throw notFound(`User ${id} not found`) if (resp.status === 401) throw unauthorized('Not authenticated') return resp.json() } ) return [errors, user] } // Inspecting metadata later: const [errors, user] = await loadUser(999) if (errors) { const meta = getErrorMetadata(errors) if (meta && meta.status === 404) { console.log('User does not exist') } } ``` --- ## .response() — Partial Success Pattern When a process partially succeeds, you want both errors AND valid data. All wrapping error APIs support `.response(data)` to attach data to the thrown error. ### Example ```js const processItems = (items) => catchErrors( 'processItems failed', async () => { const results = [] const failures = [] for (const item of items) { const [err, data] = await catchErrors(() => transform(item)) if (err) failures.push(...err) else results.push(data) } if (failures.length) throw wrapErrors('Some items failed', failures).response(results) return results } ) // Consumer receives BOTH errors and data: const [errors, data] = await processItems(myItems) if (errors) console.error(`${errors.length} items failed`) if (data) console.log(`${data.length} items succeeded`) // Both can be truthy at the same time! ``` --- ## getErrorMetadata Extracts and merges metadata from all errors in an array. ### Signature ``` getErrorMetadata(errors: Error[]) → Object|string|null ``` Returns: Merged metadata object, or null if no errors have metadata. GOTCHA: Later errors in the array override earlier errors for the same metadata keys. Since errors are ordered outermost-first, the **innermost (original cause) error's metadata wins**. ### Example ```js const err1 = wrapCustomErrors({ code: 'WRAPPER', status: 500 })('Outer error') const err2 = wrapCustomErrors({ code: 'ROOT_CAUSE', detail: 'timeout' })('Inner error') const meta = getErrorMetadata([err1, err2]) // meta = { code: 'ROOT_CAUSE', status: 500, detail: 'timeout' } // ^ 'ROOT_CAUSE' from err2 overrides 'WRAPPER' from err1 // ^ status: 500 kept from err1 (err2 has no status key) // ^ detail: 'timeout' added from err2 ``` --- ## mergeErrors Combines an array of Error objects into a single Error with concatenated messages and stacks. ### Signature ``` mergeErrors(errors: Error[]) → Error ``` ### Example ```js const combined = mergeErrors([ new Error('Step 1 failed'), new Error('Step 2 failed') ]) console.error(combined.message) // 'Step 1 failed' console.error(combined.stack) // Both stacks concatenated with newlines ``` --- ## required Validates that all specified properties are truthy. Throws immediately if any value is falsy (null, undefined, '', 0, false). ### Signature ``` required(input: Object) → void (throws Error if any value is falsy) required(input: any) → void (throws Error if input itself is falsy) ``` ### Example ```js const createUser = ({ name, email }) => catchErrors( 'createUser failed', () => { required({ name, email }) // If name or email is falsy, throws: // Error("Missing required argument 'name'") or // Error("Missing required argument 'email'") return saveUser({ name, email }) } ) // Supports dot notation for descriptive keys: const createOrder = ({ user, items }) => catchErrors( 'createOrder failed', () => { required({ 'user.id': user?.id, 'user.email': user?.email, 'items': items }) // Throws: Error("Missing required argument 'user.id'") return processOrder({ user, items }) } ) ``` GOTCHA: `required` checks falsiness, not just null/undefined. The number `0`, empty string `''`, and `false` will ALL trigger the error. If `0` is a valid value for your use case, do not use `required` for that field. --- ## Complete Pattern — Putting It All Together This example shows the canonical way to compose multiple fallible operations: ```js const { error: { catchErrors, wrapErrors: e, required } } = require('puffy-core') const createOrder = ({ userId, productId, quantity }) => catchErrors( 'createOrder failed', async () => { // 1. Validate inputs required({ userId, productId, quantity }) // 2. Load user const [userErr, user] = await catchErrors( `Failed to load user ${userId}`, async () => { const resp = await fetch(`/api/users/${userId}`) if (!resp.ok) throw new Error(`HTTP ${resp.status}`) return resp.json() } ) if (userErr) throw e('User lookup failed', userErr) // 3. Load product const [productErr, product] = await catchErrors( `Failed to load product ${productId}`, async () => { const resp = await fetch(`/api/products/${productId}`) if (!resp.ok) throw new Error(`HTTP ${resp.status}`) return resp.json() } ) if (productErr) throw e('Product lookup failed', productErr) // 4. Create order const [orderErr, order] = await catchErrors( 'Failed to create order', async () => { const resp = await fetch('/api/orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: user.id, productId: product.id, quantity, total: product.price * quantity }) }) if (!resp.ok) throw new Error(`HTTP ${resp.status}`) return resp.json() } ) if (orderErr) throw e('Order creation failed', orderErr) return order } ) // Usage: const [errors, order] = await createOrder({ userId: 1, productId: 42, quantity: 3 }) if (errors) { // Full error trace from outermost to innermost: // ['createOrder failed', 'Product lookup failed', 'Failed to load product 42', 'HTTP 404'] errors.forEach(err => console.error(err.message)) } else { console.log('Order created:', order.id) } ``` --- ## DO / DON'T Quick Reference ```js // DO: Always destructure the response const [errors, data] = catchErrors(...) const [errors, data] = await catchErrors(...) // DON'T: Use the response as a single object const result = catchErrors(...) result.data // WRONG — no .data property result[1] // Works but unclear // DO: Always await async catchErrors const [errors, data] = await catchErrors(async () => {...}) const [errors, data] = await catchErrors(somePromise) // DON'T: Forget await on async const [errors, data] = catchErrors(async () => {...}) // errors is now a Promise, not an error array! // DO: Always provide a context message const [errors, data] = catchErrors('Failed to load user', async () => {...}) // DON'T: Skip the context message const [errors, data] = catchErrors(async () => {...}) // Error stack will lack context // DO: Check errors before using data const [errors, data] = await catchErrors('load user', loadUser(id)) if (errors) throw e('user step failed', errors) console.log(data.name) // Safe — errors was null // DON'T: Use data without checking errors const [errors, data] = await catchErrors('load user', loadUser(id)) console.log(data.name) // CRASH if data is undefined due to errors // DO: Pass promises directly or use async arrow const [errors, data] = await catchErrors(fetch('/api')) const [errors, data] = await catchErrors(async () => { return await fetch('/api') }) // DON'T: Wrap a promise in a sync arrow const [errors, data] = catchErrors(() => fetch('/api')) // data is a Promise object, not the fetch result! ```