> ## Documentation Index
> Fetch the complete documentation index at: https://cofhe-docs.fhenix.zone/llms.txt
> Use this file to discover all available pages before exploring further.

# Error Handling

> Handle errors from @cofhe/sdk operations using typed CofheError objects

All SDK operations return values directly and throw typed `CofheError` objects on failure. This replaces the `Result` wrapper pattern used by `cofhejs`.

## Catching errors

```typescript theme={null}
import { isCofheError, CofheErrorCode, Encryptable } from '@cofhe/sdk';

try {
  const [encrypted] = await client
    .encryptInputs([Encryptable.uint32(42n)])
    .execute();
} catch (err) {
  if (isCofheError(err)) {
    console.error(err.code);    // CofheErrorCode enum value
    console.error(err.message); // human-readable description
  }
}
```

## What a CofheError carries

Two fields are always set:

* `code: CofheErrorCode` — the enum value identifying the error type
* `message: string` — a human-readable description of what went wrong

Four more are declared optional on the class. The property always exists; its value is `undefined` unless the code that threw supplied it, so narrow before using it:

* `hint?: string` — an actionable suggestion for fixing the error. Set by most SDK throw sites, but not all
* `context?: Record<string, unknown>` — the state that produced the error. For a `ZkPackFailed` from oversized inputs this holds `totalBits`, `maxBits`, and the offending `items`
* `cause?: Error` — the inner error being wrapped. When set, its message is also appended to `message` as `| Caused by: ...`
* `apiErrorCode?: string` — the raw backend error string. Set only on errors built from a decryption backend response

Because the four are optional, guard each one rather than assuming it is there:

```typescript theme={null}
try {
  await client.encryptInputs([Encryptable.uint32(42n)]).execute();
} catch (err) {
  if (isCofheError(err)) {
    console.error(err.code, err.message);
    if (err.hint) console.error('Hint:', err.hint);
    if (err.context) console.error('Context:', err.context);
  }
}
```

Use `isCofheError(err)` to check if a caught error is a `CofheError`.

## Common error codes

| Error code                       | When it occurs                                                                                 |
| -------------------------------- | ---------------------------------------------------------------------------------------------- |
| `ZkPackFailed`                   | `encryptInputs` exceeded the 2048-bit total plaintext limit, or an item had an invalid `utype` |
| `ConsumingContractUninitialized` | `execute()` was called without `setConsumingContract(...)`                                     |
| `ACPNotFound`                    | No ACP found for the given `chainId + account`                                                 |
| `ACPInvalid`                     | The ACP signature is invalid                                                                   |
| `ACPMalformed`                   | The decryption backend could not parse the submitted ACP                                       |
| `ACPExpired`                     | The ACP is past its expiration                                                                 |
| `ACPRevoked`                     | The issuer revoked this ACP                                                                    |
| `ACPDenied`                      | The ACP does not cover the requested handle                                                    |
| `ACPRequired`                    | The flow needs an ACP and none was supplied                                                    |
| `DecryptFailed`                  | The decryption request was rejected, or the response came back malformed                       |
| `NotConnected`                   | Attempted an operation before calling `client.connect(...)`                                    |

The first two codes above are raised locally by the SDK. The `ACP*` codes other than `ACPNotFound` are stable codes returned by the decryption backend and mapped onto the enum, so those errors also carry `apiErrorCode`.

<Note>
  **Migrating from 0.6.x:** the `Permit*` codes are gone. `PermitNotFound` is now `ACPNotFound`, `InvalidPermitData` is `InvalidACPData`, `InvalidPermitDomain` is `InvalidACPDomain`, and `CannotRemoveLastPermit` is `CannotRemoveLastACP`. The set also expanded: expiry, revocation, and scope denial each have their own code, so you no longer have to infer which one applied from the message.
</Note>

## Error handling patterns

### Encryption errors

```typescript theme={null}
import { isCofheError, CofheErrorCode, Encryptable } from '@cofhe/sdk';

try {
  const encrypted = await client
    .encryptInputs([Encryptable.uint128(veryLargeValue)])
    .setConsumingContract(contractAddress)
    .execute();
} catch (err) {
  if (isCofheError(err) && err.code === CofheErrorCode.ZkPackFailed) {
    console.error('Input too large, split into multiple calls');
  }
}
```

### Decryption errors

```typescript theme={null}
import { isCofheError, CofheErrorCode, FheTypes } from '@cofhe/sdk';

try {
  const plaintext = await client
    .decryptForView(ctHash, FheTypes.Uint32)
    .execute();
} catch (err) {
  if (isCofheError(err) && err.code === CofheErrorCode.ACPNotFound) {
    // Create an ACP and retry
    await client.acp.getOrCreateSelfACP();
    const plaintext = await client
      .decryptForView(ctHash, FheTypes.Uint32)
      .execute();
  }
}
```

### Distinguishing why an ACP is invalid

The decrypt flows call `ACPUtils.validate(acp)` internally before submitting the request to the decryption backend. That helper enforces **schema + signed + not-expired** all at once, so when it fails the recovery path depends on **which** check tripped.

Use the non-throwing `ValidationUtils.isValid` helper from `@cofhe/sdk/acps` to pre-flight the active ACP and route based on the typed reason, which avoids the thrown error path entirely:

```typescript theme={null}
import { FheTypes } from '@cofhe/sdk';
import { ValidationUtils } from '@cofhe/sdk/acps';

const active = client.acp.getActiveACP();
const result = active
  ? ValidationUtils.isValid(active)
  : { valid: false, error: 'not-signed' as const };

if (!result.valid) {
  switch (result.error) {
    case 'expired':
      await client.acp.getOrCreateSelfACP(); // create a fresh one
      break;
    case 'not-signed':
      await client.acp.getOrCreateSelfACP(); // prompt the wallet to sign
      break;
    case 'invalid-schema':
      client.acp.removeActiveACP(); // stored payload is malformed
      break;
  }
}

const plaintext = await client
  .decryptForView(ctHash, FheTypes.Uint32)
  .execute();
```

`ValidationResult.error` is the typed union `'invalid-schema' | 'expired' | 'not-signed' | null`. See [validating ACPs](/client-sdk/guides/acps#validating) for the full helper surface.

<Note>
  If you prefer the throwing path: `ACPUtils.validate(acp)` raises plain `Error`s with messages `ACP is expired` / `ACP is not signed` (or a Zod schema error). These are not wrapped in `CofheError`, so use `err.message` rather than an error code to branch.
</Note>
