# SDK errors

`captureCDR()` rejects rather than returning a partial result. If it resolves, the evidence is stored.

Errors are `ExpressConsentError` instances carrying a `code`, a `message`, and sometimes `details`.
Every one is also dispatched on `window` as an `expressconsent:error` event, except
`CAPTURE_NOT_IN_BROWSER`, where there is no `window` to dispatch on.

```javascript
try {
  await window.ExpressConsent.captureCDR();
} catch (error) {
  console.error(error.code, error.message, error.details?.backendCode);
}
```

## Handle them the same way

Whatever the cause, the response is the same: log it and let the person's submission proceed. Never
block a consumer from submitting because evidence capture failed.

## Codes

| Code | Cause |
|---|---|
| `CONFIG_CID_MISSING` | No organization identifier: no `data-ec-cid`, no `?cid=` on the script URL, nothing on `window.__ExpressConsentConfig`, and no `cid` passed to the call. A setup error; it will fail on every capture. |
| `UPLOAD_FAILED` | The server rejected the upload. `details.backendCode` says why; see below. |
| `INTERNAL_ERROR` | The capture failed locally: a network failure, a timeout, or an error while serializing the page. |
| `CONFIG_API_BASE_UNRESOLVED` | The upload endpoint could not be resolved, because the SDK was not served from an ExpressConsent host. Load it from `sdk.expressconsent.com`; a self-hosted, proxied, or bundled copy cannot work out where to upload. |
| `CAPTURE_NOT_IN_BROWSER` | No `window` or `document`. You are calling it during server-side rendering. |

`NETWORK_ERROR` appears in the exported `ExpressConsentErrorCode` type but is never thrown. Network
failures arrive as `INTERNAL_ERROR`.

`error.details` carries `backendCode` and `backendMessage`, and only on `UPLOAD_FAILED`. Route
`backendCode` into your monitoring; it is the only thing that says why an upload was refused.

> **Info: A missing script is a TypeError, not an SDK error**
>
> If the bundle has not run, `window.ExpressConsent` is `undefined` and the property access throws a
> `TypeError`, which the same `try`/`catch` handles. The same happens when the script has not yet
> finished loading: with `async`, a very early submission can beat it. Guard with
> `if (window.ExpressConsent)` if your form can be submitted in the first moments of page load.

## Upload rejection reasons

When the code is `UPLOAD_FAILED`, `details.backendCode` carries the server's reason.

| `backendCode` | Meaning |
|---|---|
| `CAPTURE_DISABLED` | Capture is switched off for your organization. Contact us. |
| `PAYLOAD_TOO_LARGE` | Over the 3 MiB limit. Usually `inlineAssets` left on in production, or an unusually large page. |
| `PAYLOAD_TOO_SMALL` | The upload body was effectively empty. |
| `INTEGRITY_FAILURE` | The uploaded bytes did not match their checksum, or did not match the fingerprint recorded when the capture was taken. Not retried. Usually a proxy or browser extension rewriting the request body. |
| `IDEMPOTENCY_CONFLICT` | Different content was uploaded under a record identifier that already exists. |
| `MISSING_HEADER`, `INVALID_HEADER` | A malformed request. Despite the names these are about the upload's query parameters, HTTP method, and content type rather than HTTP headers. Indicates a bug, or a proxy rewriting the upload. |
| `MISSING_COMMIT` | A stored record was re-sent but its chain of custody could not be established, so it was refused. |
| `SERVER_ERROR` | Our side failed. Retried automatically. |
| `INVALID_ENVELOPE`, `INVALID_SNAPSHOT` | Defined but not currently returned. A body the server cannot decode comes back as `INVALID_HEADER`. |

## What to do about a record you never got

A capture that fails locally can still arrive later, re-sent from the browser on the person's next
visit, long after your code gave up waiting and moved on without a `cdrId`.

Pass your own identifier in `custom` on every capture, and reconcile from the webhook or by filtering
the API on that value.
