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

# Errors

# Errors & fail-open semantics

```text theme={null}
Error
└── BlursecError                (all SDK errors extend this)
    ├── BlursecValidationError  (your code called the SDK wrong)
    ├── BlursecAPIError         (the API returned a non-2xx response)
    ├── BlursecTimeoutError     (the request was aborted by timeout or caller)
    └── BlursecCircuitOpenError (the local circuit breaker is open)
```

Every error preserves the original `cause` (per ES2022 `Error.cause`) so you can chain through to the underlying failure when reporting.

***

## `BlursecError`

Base class. You'll rarely instantiate it directly, but you can use it for catch-all branches:

```ts theme={null}
import { BlursecError } from "@blursec/sdk";

try {
  await blursec.auth.rotate();
} catch (err) {
  if (err instanceof BlursecError) {
    metrics.increment("blursec.error", { kind: err.name });
  }
  throw err;
}
```

***

## `BlursecValidationError`

Thrown synchronously when you call the SDK with invalid arguments. Examples:

* `new Blursec({ apiKey: "" })`
* `blursec.credentials.checkHash("not-a-valid-hex")`
* Negative or non-finite `timeoutMs`

These always indicate a bug in your integration. Don't catch and retry — fix the call site.

***

## `BlursecAPIError`

Thrown when the API returns any non-2xx response. Carries:

```ts theme={null}
interface BlursecAPIError extends BlursecError {
  status: number;             // HTTP status
  statusText: string;
  code?: string;              // API error code (if returned)
  requestId?: string;         // x-request-id / x-blursec-request-id
  body: unknown;              // parsed JSON or raw text
  url: string;
  method: string;
  cause?: unknown;
}
```

```ts theme={null}
import { BlursecAPIError } from "@blursec/sdk";

try {
  await blursec.auth.whoami();
} catch (err) {
  if (err instanceof BlursecAPIError) {
    if (err.status === 401) throw new Error("Bad Blursec API key");
    console.error(`[blursec] ${err.status} ${err.url} (request ${err.requestId})`);
  }
  throw err;
}
```

### When the SDK swallows vs. throws `BlursecAPIError`

For `blursec.credentials.*` and `blursec.sessions.verify()` calls, **fail-open** swallows `5xx` errors and returns a safe default. **`4xx`** always throws — those indicate a caller bug (bad key, malformed request, etc.) and silently allowing the login would mask the misconfiguration.

For every other call (`auth.*`, `client.request()`), every non-2xx throws.

***

## `BlursecTimeoutError`

Thrown when a request is aborted — either by the SDK's internal timeout or by a caller-supplied `AbortSignal`.

```ts theme={null}
interface BlursecTimeoutError extends BlursecError {
  timeoutMs?: number;        // populated for SDK-initiated timeouts
  cause?: unknown;           // usually a DOMException("...", "AbortError")
}
```

```ts theme={null}
import { BlursecTimeoutError } from "@blursec/sdk";

try {
  await blursec.auth.whoami({ /* ... */ });
} catch (err) {
  if (err instanceof BlursecTimeoutError) {
    metrics.increment("blursec.timeout", { ms: err.timeoutMs });
  }
  throw err;
}
```

***

## `BlursecCircuitOpenError`

Thrown by the internal circuit breaker when too many recent requests failed and the breaker has tripped **open**. While open, the SDK short-circuits new requests instead of hammering a struggling API.

```ts theme={null}
interface BlursecCircuitOpenError extends BlursecError {
  retryAfterMs: number;      // ms until the next probe is permitted
}
```

For `credentials.*` / `sessions.verify` this is swallowed by fail-open (the result carries `failedOpen: true`, `failureReason: "circuit_open"`). You can inspect breaker state for a `/health` endpoint via the exported `CircuitBreaker`.

***

## The fail-open contract (`credentials.*` + `sessions.verify`)

By default, `blursec.credentials.check()`, `checkToken()`, `checkHash()`, and `blursec.sessions.verify()` **never throw on transient failures**. Instead they return:

```ts theme={null}
{
  leaked: false,
  severity: null,
  recommendedAction: "allow",
  failedOpen: true,           // ← inspect this
  /* ...rest of CredentialRiskResult */
}
```

Cases that fail open:

| Cause                                          | Fails open? | Notes                              |
| ---------------------------------------------- | ----------- | ---------------------------------- |
| `BlursecTimeoutError`                          | ✅           | SDK timed out waiting for the API. |
| `BlursecAPIError` with `status >= 500`         | ✅           | API is down or overloaded.         |
| `BlursecCircuitOpenError`                      | ✅           | Local breaker tripped open.        |
| Generic network error (DNS, TLS, socket reset) | ✅           | Treated as "we couldn't ask".      |
| `BlursecAPIError` with `status 4xx`            | ❌ throws    | Caller bug — surface it.           |
| `BlursecValidationError`                       | ❌ throws    | Caller bug — surface it.           |

Always check `risk.failedOpen` if you care about distinguishing "definitely safe" from "we don't know":

```ts theme={null}
const risk = await blursec.checkCredential(email, password);

if (risk.failedOpen) {
  metrics.increment("blursec.failed_open");
  // proceed with the login — but maybe log extra context
}

if (risk.leaked) {
  return res.status(403).end();
}
```

### Opting out

Set `failOpen: false` for any single call to switch to throw-on-error semantics:

```ts theme={null}
try {
  const risk = await blursec.credentials.check(email, password, {
    failOpen: false,
    timeoutMs: 500,
  });
  // result is guaranteed to reflect a real API response
} catch (err) {
  // BlursecTimeoutError, BlursecAPIError (any status), or network Error
}
```

Recommended only when:

* You're running offline tests and want deterministic behavior
* You have a fallback risk source and want to surface Blursec failures explicitly
* You're auditing the integration and need to see every error

For production login flows, the default (`failOpen: true`) is almost always correct.
