
# Errors

> One class, one code set, one subpath. Branch on `error.code`, never on the message.

::note
**ELI5.** When something goes wrong, this library throws one kind of error, and that error carries a short label saying what kind of wrong it was. Read the label, not the sentence: the sentence is for a human reading a log and may be reworded, while the label is a promise. And note what does _not_ throw: a bad signature, a wrong code, a missing header. Those come back as `false`, because a request being wrong is not an exceptional event on a server.
::

```ts
import { UnsecureError } from "unsecure/errors";
```

Everything the library throws is an `UnsecureError`. A native `TypeError`, `RangeError` or `SyntaxError` escaping from `unsecure` is a bug. Report it.

## Signature

```ts
type UnsecureErrorCode =
  "INVALID_TYPE" | "OUT_OF_RANGE" | "MALFORMED" | "UNSUPPORTED" | "FROZEN" | "PLATFORM";

class UnsecureError extends Error {
  readonly name: "UnsecureError";
  readonly code: UnsecureErrorCode;
  readonly cause?: unknown;
  constructor(code: UnsecureErrorCode, message: string, options?: { cause?: unknown });
}
```

`message` names the function, the value it judged and what it expected, as in `"hkdf: length must be an integer between 1 and 8160, got 0."`. `code` is that same judgement, machine-readable. `cause` is set only when the failure came from outside the library.

## The codes

| Code           | Meaning                                                                              | Where it comes from                                                                                                                                                                                                                                   |
| -------------- | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INVALID_TYPE` | A value of the wrong JavaScript type, or nothing where a value is required.          | Anything taking a `BytesSource`; `secureCompare`'s `expected`; a non-string `algorithm`; `otpauthURI`'s `type`, `account` and `issuer`; `uuidv7`'s `timestamp`; every codec `stringify` and `parse`.                                                  |
| `OUT_OF_RANGE` | The right type, outside its documented domain.                                       | An empty secret; every bounded number in `hkdf`, `otp`, `generate` and `random`; a `CryptoKey` of the wrong kind; a base32 `alphabet` that is not 32 distinct ASCII characters; overlapping generator character sets; `secureCompare` under `strict`. |
| `MALFORMED`    | Text that is not what it claims to be.                                               | A codec `parse` on anything but a canonical encoding, or decoded bytes that are not valid UTF-8; `safeJsonParse` on JSON that does not parse; `uuidv7Timestamp` on a non-v7 string; `argon2Verify` on a string that is not a PHC string.              |
| `UNSUPPORTED`  | A name outside the set the library accepts.                                          | Any `algorithm`; any `returnAs`; `otpauthURI` for a `type` other than `"hotp"` or `"totp"`; an unknown Argon2 variant or a PHC version other than `0x13`.                                                                                             |
| `FROZEN`       | A dangerous key cannot be removed because the object holding it is frozen or sealed. | `sanitizeObject`, and `safeJsonParse` through it.                                                                                                                                                                                                     |
| `PLATFORM`     | The runtime's Web Crypto refused an operation the library had already validated.     | `hash`, `hmac`, `hmacVerify`, `hkdf` and the OTP functions built on them. `cause` carries the platform error.                                                                                                                                         |

The union is complete for this release. A later minor may add a code, so keep a `default` branch in any `switch` over it.

## What throws and what does not

Verification functions never throw for untrusted input. [`secureCompare`](/generate/secrets/compare), [`hmacVerify`](/crypto/hashing/hmac), [`hotpVerify`, `totpVerify`](/crypto/otp) and [`argon2Verify`](/crypto/password-hashing) return `false` — or `{ valid: false }` — for a missing, malformed or wrong-typed value off the wire: a `null` header, a signature that is not canonical hex, a code that is not a string.

They throw only for something you control: an empty secret, an unsupported algorithm, an out-of-range window, a stored hash in a format nobody wrote.

So a `catch` around a verify is about your own configuration, never about the request.

## Catching

```ts
import { UnsecureError } from "unsecure/errors";
import { hmacVerify } from "unsecure/hmac";

try {
  const valid = await hmacVerify(secret, body, request.headers.get("x-signature"));
  return valid ? handle(body) : respond(403);
} catch (error) {
  if (!(error instanceof UnsecureError)) throw error;
  switch (error.code) {
    case "OUT_OF_RANGE": {
      // An empty secret: the deployment is misconfigured, the request is fine
      return respond(500);
    }
    case "PLATFORM": {
      // error.cause is the runtime's own failure
      return respond(503);
    }
    default: {
      return respond(400, { reason: error.code });
    }
  }
}
```

The codecs report exactly where the text stopped being canonical, which makes their message safe and useful to return:

```ts
import { UnsecureError, base64Parse } from "unsecure";

try {
  base64Parse(untrusted);
} catch (error) {
  if (error instanceof UnsecureError && error.code === "MALFORMED") {
    return respond(400, { reason: error.message });
  }
  throw error;
}
```

## Mapping codes onto HTTP

A reasonable default, to adapt:

| Code           | Status     | Why                                                                             |
| -------------- | ---------- | ------------------------------------------------------------------------------- |
| `MALFORMED`    | 400        | The client sent text that is not what it claimed to be.                         |
| `INVALID_TYPE` | 400 or 500 | 400 if the value came from the request body; 500 if it came from your own code. |
| `OUT_OF_RANGE` | 500        | Usually a configuration value that did not load.                                |
| `UNSUPPORTED`  | 500        | You named an algorithm or a `returnAs` the library does not have.               |
| `FROZEN`       | 500        | A frozen object reached a sanitizer. Use `sanitizeObjectCopy`.                  |
| `PLATFORM`     | 503        | The runtime refused an operation it should have performed.                      |

## Pitfall: matching on message text

```ts
// Breaks the moment the wording improves
if (error.message.includes("must not be empty")) { … }

// Codes are the contract
if (error instanceof UnsecureError && error.code === "OUT_OF_RANGE") { … }
```

Several messages gained a function-name prefix in 0.3. See [Migrating to 0.3](/getting-started/migration/to-0.3#1-every-error-is-an-unsecureerror).

## Pitfall: an exhaustive switch with no default

The code union is documented as growable. A `switch` with no `default` compiles today and silently does nothing the day a code is added.

```ts
switch (error.code) {
  case "MALFORMED":
    return respond(400);
  default:
    return respond(500);
}
```

## Pitfall: losing `cause`

`PLATFORM` errors, and `safeJsonParse`'s `MALFORMED`, carry the original failure in `cause`. Log it. The library's message says which operation was refused; `cause` says why.
