# Error Handling Guide

A comprehensive guide to puffy-core's functional error handling system. This is the most complex subsystem in the library and the foundation for robust error management across all modules.

## Table of Contents
- [Overview](#overview)
- [The PuffyResponse Pattern](#the-puffyresponse-pattern)
- [catchErrors](#catcherrors)
  - [Synchronous Functions](#synchronous-functions)
  - [Asynchronous Functions](#asynchronous-functions)
  - [Promises](#promises)
  - [With Context Message](#with-context-message)
- [Wrapping Errors](#wrapping-errors)
  - [wrapErrors](#wraperrors)
  - [wrapErrorsFn](#wraperrorsfn)
  - [wrapCustomErrors](#wrapcustomerrors)
- [Adding Responses to Errors](#adding-responses-to-errors)
- [Merging and Inspecting Errors](#merging-and-inspecting-errors)
  - [mergeErrors](#mergeerrors)
  - [getErrorMetadata](#geterrormetadata)
- [Required Fields Validation](#required-fields-validation)
- [Chaining with chainAsync and chainSync](#chaining-with-chainasync-and-chainsync)
- [Related Documentation](#related-documentation)

## Overview

Instead of throwing exceptions, puffy-core accumulates errors functionally. Every operation returns a `[errors, data]` tuple (a `PuffyResponse`). It is up to the engineer to decide what to do with the errors — propagate, merge, log, or ignore.

Errors are accumulated in **inverse order of occurrence**: the first error is the highest in the stack (most context), and the last error is the original cause.

```js
// ES Modules
import { catchErrors, wrapErrors, mergeErrors } from 'puffy-core/error'

// CommonJS
const { error: { catchErrors, wrapErrors, mergeErrors } } = require('puffy-core')
```

## The PuffyResponse Pattern

`PuffyResponse` is a custom `Array` subclass used as the return type for all error-handling functions. It always contains exactly two elements: `[errors, data]`.

```js
const [errors, data] = catchErrors(() => doSomething())

if (errors) {
    // errors is an array of Error objects
    console.error(errors.map(e => e.message).join('\n'))
} else {
    // data is the return value of doSomething()
    console.log(data)
}
```

Key behavior: `chainAsync` and `chainSync` use `instanceof PuffyResponse` to detect error tuples. Plain arrays are **not** treated as error responses. See [Gotchas > Error Handling](gotchas/error-handling.md) for details.

## catchErrors

The universal error wrapper. Accepts sync functions, async functions, promises, and optionally a context message.

### Synchronous Functions

```js
const [errors, data] = catchErrors(() => {
    return 'hello'
})
// errors = null, data = 'hello'

const [errors, data] = catchErrors(() => {
    throw new Error('Boom')
})
// errors = [Error('Boom')], data = undefined
```

### Asynchronous Functions

```js
const [errors, data] = await catchErrors(async () => {
    const result = await fetchSomething()
    return result
})
```

### Promises

```js
const [errors, data] = await catchErrors(somePromise)
```

### With Context Message

Add a wrapper message for stack trace clarity:

```js
const [errors, data] = await catchErrors(
    'Failed to fetch user profile',
    async () => fetchProfile(userId)
)
// On failure: errors = [Error('Failed to fetch user profile'), Error('Network timeout')]
```

## Wrapping Errors

### wrapErrors

Creates a new error with nested child errors for richer stack traces:

```js
import { catchErrors, wrapErrors } from 'puffy-core/error'

const [errors, data] = await catchErrors(async () => {
    const [innerErrors, result] = await catchErrors(riskyOperation())
    if (innerErrors)
        throw wrapErrors('Operation context failed', innerErrors)
    return result
})
```

`wrapErrors` accepts any mix of strings, Errors, and arrays:

```js
wrapErrors('Context', new Error('A'), [new Error('B'), new Error('C')])
```

### wrapErrorsFn

Sugar for creating a reusable error wrapper:

```js
import { wrapErrorsFn } from 'puffy-core/error'

// These are equivalent:
const e = wrapErrorsFn('API call failed')
throw e(caughtErrors)

throw wrapErrors('API call failed', caughtErrors)
```

### wrapCustomErrors

Attaches metadata to errors for programmatic inspection:

```js
import { catchErrors, wrapCustomErrors } from 'puffy-core/error'

const fail = () => {
    throw wrapCustomErrors({ code: 'NOT_FOUND', status: 404 })('User not found')
}

const [errors] = catchErrors(fail)
// errors[0].metadata = { code: 'NOT_FOUND', status: 404 }
```

## Adding Responses to Errors

For partial-success scenarios where both errors and valid data exist, use the `.response()` method on wrapped errors:

```js
import { catchErrors, wrapErrors } from 'puffy-core/error'

const processItems = items => catchErrors((async () => {
    const results = []
    const failures = []

    for (const item of items) {
        const [err, data] = await catchErrors(() => process(item))
        if (err) failures.push(...err)
        else results.push(data)
    }

    if (failures.length)
        throw wrapErrors('Some items failed', failures).response(results)

    return results
})())

// Consumer gets both:
const [errors, data] = await processItems(myItems)
if (errors) handleErrors(errors)
if (data) handleResults(data)  // data contains the successful results
```

## Merging and Inspecting Errors

### mergeErrors

Combines an array of errors into a single Error with a joined message:

```js
import { mergeErrors } from 'puffy-core/error'

const combined = mergeErrors([new Error('A'), new Error('B')])
console.error(combined.message) // 'A\nB'
```

### getErrorMetadata

Extracts and merges metadata from all errors in an array:

```js
import { getErrorMetadata } from 'puffy-core/error'

const meta = getErrorMetadata(errors)
// Returns merged object of all error metadata
// Later errors' metadata overrides earlier for same keys
```

## Required Fields Validation

`required()` throws if any specified property is falsy (null, undefined, empty string, 0, false):

```js
import { catchErrors, required } from 'puffy-core/error'

const createUser = ({ name, email }) => catchErrors('createUser failed', () => {
    required({ name, email })
    // If name or email is falsy, throws immediately
    return saveUser({ name, email })
})
```

Supports dot notation for nested properties:

```js
required({ name, 'address.city': address?.city })
```

## Chaining with chainAsync and chainSync

The `func` module provides `chainAsync` and `chainSync` for composing operations with automatic error propagation. See [API Reference > func](api.md#func) for full signatures.

```js
import { chainAsync } from 'puffy-core/func'

const [errors, data] = await chainAsync(
    () => fetchUser(id),
    user => fetchOrders(user.id),
    orders => formatReport(orders)
)

if (errors) {
    // Chain stopped at the first failure
    console.error(errors)
} else {
    console.log(data.value) // Result of formatReport
    console.log(data.data)  // [fetchUser result, fetchOrders result, formatReport result]
}
```

## Related Documentation
- [API Reference](api.md) -- Full function signatures for `error` and `func` modules
- [Error Handling Gotchas](gotchas/error-handling.md) -- Non-obvious behavior and pitfalls
