
# Errors

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

::note
**ELI5.** When unjwt refuses a token, a key or an option, it throws one kind of error, a `JWTError`, and that error carries a short label such as `ERR_JWT_EXPIRED`. The label says what went wrong. The sentence next to it is for a human reading a log and may be reworded in any release. So your code reads the label. Some calls never throw for bad input. A type guard answers `false`, and a session with a bad cookie comes back empty.
::

`JWTError` and `isJWTError` are exported from `unjwt`, `unjwt/jws`, `unjwt/jwe` and `unjwt/jwk`. The adapter entry points do not re-export them, so a session handler imports them from one of those four.

## Signature

```ts
type KnownJWTErrorCode =
  | "ERR_JWT_EXPIRED"
  | "ERR_JWT_NBF"
  | "ERR_JWT_CLAIM_INVALID"
  | "ERR_JWT_CLAIM_MISSING"
  | "ERR_JWS_INVALID"
  | "ERR_JWS_INVALID_SERIALIZATION"
  | "ERR_JWS_SIGNATURE_INVALID"
  | "ERR_JWS_ALG_NOT_ALLOWED"
  | "ERR_JWS_ALG_MISSING"
  | "ERR_JWS_SIGNER_ALG_INFERENCE"
  | "ERR_JWS_NO_MATCHING_SIGNER"
  | "ERR_JWS_HEADER_PARAMS_NOT_DISJOINT"
  | "ERR_JWS_B64_INCONSISTENT"
  | "ERR_JWE_INVALID"
  | "ERR_JWE_INVALID_SERIALIZATION"
  | "ERR_JWE_DECRYPTION_FAILED"
  | "ERR_JWE_ALG_NOT_ALLOWED"
  | "ERR_JWE_ALG_MISSING"
  | "ERR_JWE_ALG_FORBIDDEN_IN_MULTI"
  | "ERR_JWE_ENC_MISSING"
  | "ERR_JWE_RECIPIENT_ALG_INFERENCE"
  | "ERR_JWE_NO_MATCHING_RECIPIENT"
  | "ERR_JWE_HEADER_PARAMS_NOT_DISJOINT"
  | "ERR_JWK_INVALID"
  | "ERR_JWK_KEY_NOT_FOUND"
  | "ERR_JWK_UNSUPPORTED"
  | "ERR_SESSION_TOKEN_INVALID"
  | "ERR_SESSION_READONLY_EVENT"
  | "ERR_SESSION_KEY_INVALID"
  | "ERR_SESSION_CONFIG_INVALID"
  | "ERR_INVALID_OPTION";

type JWTErrorCode = KnownJWTErrorCode | (string & {});

interface JWTErrorCauseMap {
  ERR_JWT_EXPIRED: { jti?: string; iat?: number; exp?: number };
}

class JWTError<TCode extends JWTErrorCode = JWTErrorCode> extends Error {
  readonly name: "JWTError";
  readonly code: TCode;
  readonly cause?: unknown;
  constructor(message: string, code: TCode, cause?: unknown);
}

function isJWTError<T extends keyof JWTErrorCauseMap>(
  error: unknown,
  code: T,
): error is JWTError<T> & { cause: JWTErrorCauseMap[T] };
function isJWTError<T extends KnownJWTErrorCode>(error: unknown, code: T): error is JWTError<T>;
function isJWTError(error: unknown): error is JWTError;
```

`KnownJWTErrorCode` is the closed list this release throws. `JWTErrorCode`, the type of `code`, also admits any other string, so a newer unjwt can add a code without breaking your types. Keep a fallback branch for codes you do not handle.

## The codes

| Code                                 | Meaning                                                                                                                                                                                | Raised by                                                                                                                                                                                                                        |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ERR_JWT_EXPIRED`                    | `exp` has passed, or the token is older than `maxTokenAge`.                                                                                                                            | [`verify`](/jwt/jws/verify), [`decrypt`](/jwt/jwe/decrypt), `verifyMulti`, `decryptMulti`, [`validateJwtClaims`](/utils/claims), session `verify*`/`unseal*`; given to the `onExpire` [hook](/adapters/hooks) on a session read. |
| `ERR_JWT_NBF`                        | `nbf` is in the future.                                                                                                                                                                | The same claim checks.                                                                                                                                                                                                           |
| `ERR_JWT_CLAIM_INVALID`              | `iss`, `sub` or `aud` differs from the expectation; `exp`, `nbf` or `iat` is not a non-negative number; `iat` is in the future. Also both `expiresIn` and `expiresAt` given to a sign. | Claim checks; [`sign`](/jwt/jws/sign), [`encrypt`](/jwt/jwe/encrypt).                                                                                                                                                            |
| `ERR_JWT_CLAIM_MISSING`              | A claim the options require is absent.                                                                                                                                                 | Claim checks.                                                                                                                                                                                                                    |
| `ERR_JWS_INVALID`                    | The token is malformed: wrong number of parts, a segment that is not canonical base64url, a bad header, a `typ` mismatch, a `crit` or `b64` violation, too many signatures.            | `verify`, `verifyMulti`, `verifyMultiAll`, `sign` (`b64: false` payload with a dot).                                                                                                                                             |
| `ERR_JWS_INVALID_SERIALIZATION`      | A JSON JWS that is neither General nor Flattened.                                                                                                                                      | `verifyMulti`, `verifyMultiAll`, `generalToFlattenedJWS`.                                                                                                                                                                        |
| `ERR_JWS_SIGNATURE_INVALID`          | The signature does not verify under the key.                                                                                                                                           | `verify`, `verifyMulti`.                                                                                                                                                                                                         |
| `ERR_JWS_ALG_NOT_ALLOWED`            | `alg` is `none`, unknown, outside `algorithms`, or no allowlist can be inferred from the key.                                                                                          | `verify`, `verifyMulti`, `sign`, `signMulti`.                                                                                                                                                                                    |
| `ERR_JWS_ALG_MISSING`                | `sign` cannot tell which algorithm to use.                                                                                                                                             | `sign`.                                                                                                                                                                                                                          |
| `ERR_JWS_SIGNER_ALG_INFERENCE`       | A signer's key carries no usable `alg`.                                                                                                                                                | [`signMulti`](/jwt/jws/multi).                                                                                                                                                                                                   |
| `ERR_JWS_NO_MATCHING_SIGNER`         | `strictSignerMatch` found no signature the key could have produced.                                                                                                                    | `verifyMulti`.                                                                                                                                                                                                                   |
| `ERR_JWS_HEADER_PARAMS_NOT_DISJOINT` | A name sits in both the protected and the unprotected header of a signature.                                                                                                           | `signMulti`, `verifyMulti`.                                                                                                                                                                                                      |
| `ERR_JWS_B64_INCONSISTENT`           | Signers disagree on `b64` ([RFC 7797 §3](https://www.rfc-editor.org/rfc/rfc7797#section-3)).                                                                                           | `signMulti`.                                                                                                                                                                                                                     |
| `ERR_JWE_INVALID`                    | The token or a header is malformed, `enc` is unsupported, a `crit` rule fails, a limit is exceeded.                                                                                    | `decrypt`, `decryptMulti`, `encrypt`, `encryptMulti`, [`wrapKey`, `unwrapKey`](/jwk/wrap-unwrap).                                                                                                                                |
| `ERR_JWE_INVALID_SERIALIZATION`      | A JSON JWE that is neither General nor Flattened.                                                                                                                                      | `decryptMulti`, `encryptMulti`, `generalToFlattened`.                                                                                                                                                                            |
| `ERR_JWE_DECRYPTION_FAILED`          | Key unwrap or content decryption failed. One code for every cause ([RFC 7516 §11.5](https://www.rfc-editor.org/rfc/rfc7516#section-11.5)).                                             | `decrypt`, `decryptMulti`, `unwrapKey`.                                                                                                                                                                                          |
| `ERR_JWE_ALG_NOT_ALLOWED`            | `alg` or `enc` is `none`, unknown, or outside `algorithms` / `encryptionAlgorithms`.                                                                                                   | `decrypt`, `decryptMulti`, `encrypt`, `encryptMulti`.                                                                                                                                                                            |
| `ERR_JWE_ALG_MISSING`                | `encrypt` cannot tell which key management algorithm to use.                                                                                                                           | `encrypt`.                                                                                                                                                                                                                       |
| `ERR_JWE_ENC_MISSING`                | `alg: "dir"` without an `enc`.                                                                                                                                                         | `encrypt`.                                                                                                                                                                                                                       |
| `ERR_JWE_ALG_FORBIDDEN_IN_MULTI`     | `dir` or bare `ECDH-ES` given to a multi-recipient message.                                                                                                                            | [`encryptMulti`](/jwt/jwe/multi).                                                                                                                                                                                                |
| `ERR_JWE_RECIPIENT_ALG_INFERENCE`    | A recipient key is not a JWK or carries no `alg`.                                                                                                                                      | `encryptMulti`.                                                                                                                                                                                                                  |
| `ERR_JWE_NO_MATCHING_RECIPIENT`      | `strictRecipientMatch` found no recipient the key could open.                                                                                                                          | `decryptMulti`.                                                                                                                                                                                                                  |
| `ERR_JWE_HEADER_PARAMS_NOT_DISJOINT` | A name sits in more than one header tier.                                                                                                                                              | `encryptMulti`, `decryptMulti`.                                                                                                                                                                                                  |
| `ERR_JWK_INVALID`                    | Key material is malformed or does not fit the algorithm.                                                                                                                               | [`importKey`](/jwk/import-export), `exportKey`, [`importPEM`](/jwk/pem), `thumbprint`, `assertCryptoKey`, and every sign, verify, encrypt or decrypt given such a key.                                                           |
| `ERR_JWK_KEY_NOT_FOUND`              | No key in a [JWK Set](/jwk/sets) matches, too many candidates match, or a lookup function threw or returned nothing.                                                                   | `verify`, `decrypt` and the multi variants.                                                                                                                                                                                      |
| `ERR_JWK_UNSUPPORTED`                | An algorithm, curve or PEM format the library does not implement.                                                                                                                      | [`generateKey`](/jwk/generate), `generateJWK`, `wrapKey`, `unwrapKey`, `importPEM`, `exportPEM`, [`deriveJWKFromPassword`](/jwk/password).                                                                                       |
| `ERR_SESSION_TOKEN_INVALID`          | An incoming session token failed verification or decryption.                                                                                                                           | Session reads (to `onError`), session `verify*`/`unseal*`.                                                                                                                                                                       |
| `ERR_SESSION_READONLY_EVENT`         | `update()` or `clear()` after the response has been sent.                                                                                                                              | [H3 sessions](/adapters/h3).                                                                                                                                                                                                     |
| `ERR_SESSION_KEY_INVALID`            | The session `key` is missing, a password string, the wrong shape, or does not import.                                                                                                  | Every session function.                                                                                                                                                                                                          |
| `ERR_SESSION_CONFIG_INVALID`         | A session config that cannot work: a bad cookie name, no token source, an unpinned `CryptoKey`, too many cookie chunks.                                                                | Every session function.                                                                                                                                                                                                          |
| `ERR_INVALID_OPTION`                 | An option value is malformed or out of range: a bad duration, `limits`, `typ`, a non-object header, update data that is not a plain object.                                            | Any function that takes options.                                                                                                                                                                                                 |

## details

Only `ERR_JWT_EXPIRED` carries structured data. Its `cause` holds the claims read from the token, so you can log which token expired or tell the user when. `isJWTError` narrows `cause` to that shape.

```ts
import { generateJWK } from "unjwt/jwk";
import { sign, verify, isJWTError } from "unjwt/jws";

const key = await generateJWK("HS256");
const token = await sign({ sub: "u1", jti: "t-1", iat: 1700000000, exp: 1700000600 }, key);

try {
  await verify(token, key);
} catch (error) {
  if (isJWTError(error, "ERR_JWT_EXPIRED")) {
    console.log(error.message);
    // JWT "exp" (Expiration Time) Claim validation failed: Token has expired (exp: 2023-11-14T22:23:20.000Z)
    console.log(error.cause);
    // { jti: "t-1", iat: 1700000000, exp: 1700000600 }
  }
}
```

A token rejected by `maxTokenAge` carries the same shape. When a session already loaded on the request runs past its expiry, the error handed to `onExpire` fills only `jti` and `exp`.

## cause

For every other code, `cause` is typed `unknown`. It holds the failure underneath when the error wraps one, and is `undefined` otherwise. The wrapped failures are:

- A token segment or a JWK field that is not canonical base64url: the `cause` is an `UnsecureError` with code `MALFORMED`, from the codecs unjwt bundles. See [unsecure's error codes](https://unsecure.s94.dev/safety/errors).
- A key Web Crypto refused: the platform's `DataError` or `OperationError`.
- A session token that failed to open: the `JWTError` that `verify` or `decrypt` threw.

```ts
import { generateJWK } from "unjwt/jwk";
import { sign, verify, isJWTError, isUnsecureError } from "unjwt/jws";

const key = await generateJWK("HS256");
const [header, payload, signature] = (await sign({ sub: "u1" }, key)).split(".");
const tampered = `${header}=.${payload}.${signature}`;

const error = await verify(tampered, key).catch((e: unknown) => e);
if (isJWTError(error, "ERR_JWS_INVALID")) {
  console.log(error.message); // Invalid JWS: Protected header could not be decoded.
  if (isUnsecureError(error.cause, "MALFORMED")) {
    console.log(error.cause.message); // Base64.parse: unexpected "=" padding.
  }
}
```

Check the cause with `isUnsecureError` or the `UnsecureError` exported by unjwt. unjwt bundles its own copy of unsecure, so an `UnsecureError` imported from your own `unsecure` dependency is a different class and `instanceof` against it is always `false`.

The [base64 helpers](/utils/encoding) in `unjwt/utils` are the one place an `UnsecureError` is thrown directly, without a `JWTError` around it.

A session read that fails hands its error to `onError` instead of throwing. Its `cause` is the token error:

```ts
import { H3 } from "h3";
import { useJWSSession, generateJWK } from "unjwt/adapters/h3v2";
import { isJWTError } from "unjwt/jws";

const key = await generateJWK("HS256");

const app = new H3().get("/", async (event) => {
  const session = await useJWSSession(event, {
    key,
    hooks: {
      onError({ error }) {
        if (isJWTError(error, "ERR_SESSION_TOKEN_INVALID") && isJWTError(error.cause)) {
          console.log(error.message); // [unjwt/h3] Invalid session token: Invalid JWS: Must contain three parts separated by dots.
          console.log(error.cause.code); // ERR_JWS_INVALID
        }
      },
    },
  });
  return { id: session.id ?? null };
});

const request = new Request("http://localhost/", { headers: { cookie: "h3-jws=not-a-token" } });
console.log(await (await app.fetch(request)).json()); // { id: null }
```

## Branching

Pass the code to `isJWTError`. It narrows `code`, and `cause` too when the code has [details](#details). Called with no code, it only checks for a `JWTError`. Messages name the failing claim, header or option and may change wording between releases; log them, branch on the code.

```ts
import { generateJWK } from "unjwt/jwk";
import { sign, verify, isJWTError } from "unjwt/jws";

const key = await generateJWK("HS256");
const otherKey = await generateJWK("HS256");
const token = await sign({ sub: "u1" }, key);

async function check(token: string, options?: Parameters<typeof verify>[2]) {
  try {
    await verify(token, key, options);
    return "ok";
  } catch (error) {
    if (isJWTError(error, "ERR_JWT_EXPIRED")) return `expired: ${error.cause.exp}`;
    if (isJWTError(error, "ERR_JWS_SIGNATURE_INVALID")) return "bad signature";
    if (isJWTError(error, "ERR_JWS_ALG_NOT_ALLOWED")) return `refused: ${error.message}`;
    if (isJWTError(error)) return `${error.code}: ${error.message}`;
    throw error;
  }
}

console.log(await check(token)); // ok
console.log(await check(await sign({ sub: "u1" }, otherKey))); // bad signature
console.log(await check(token, { algorithms: ["HS512"] })); // refused: Algorithm not allowed: HS256
console.log(await check("abc.def")); // ERR_JWS_INVALID: Invalid JWS: Must contain three parts separated by dots.
console.log(await check(await sign({ exp: 1700000600 }, key))); // expired: 1700000600
```

The code argument is a `KnownJWTErrorCode`, so a typo fails to compile instead of never matching:

```ts
if (isJWTError(error, "ERR_JWS_INVALD")) {
}
// error TS2769: No overload matches this call.
//   Argument of type '"ERR_JWS_INVALD"' is not assignable to parameter of type 'KnownJWTErrorCode'.
```

## What does not throw

These return a value for bad input instead of throwing:

- The [type guards](/utils/guards) `isJWK`, `isJWKSet`, `isCryptoKey`, `isSymmetricJWK`, `isPrivateJWK`, `isPublicJWK` and the others return `false`. `isJWTContent` returns `false` for a non-string `typ` or `cty`.
- `inferJWSAllowedAlgorithms` and `inferJWEAllowedAlgorithms` return `undefined` when a key says nothing about its algorithm, or declares an `alg` of the other family.
- [`getJWKsFromSet`](/jwk/sets) drops entries that are not JWKs.
- [`verifyMultiAll`](/jwt/jws/multi) records each signature's failure as `{ verified: false, error }`. It throws only for a broken envelope (`ERR_JWS_INVALID_SERIALIZATION`) or too many signatures (`ERR_JWS_INVALID`).
- A session read (`useJWSSession`, `getJWSSession` and their JWE and Elysia counterparts) with a bad or expired token returns an empty session, `id` `undefined`, and reports through `onError` or `onExpire`. A bad key or config still throws.
- `getJWSSessionToken` and `getJWESessionToken` return `undefined` when the request carries no token.
- `isJWTError` and `isUnsecureError` return `false` for anything else, including `null`.
