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

# Handle validation errors

> Render inline errors, gate submission on validity, and layer custom rules.

Every element surfaces validation errors on two channels: `onChange.errors` while the shopper is typing, and `onError.fieldErrors` after a rejected `submit()`. Both use the same `FieldError` shape, so a single renderer covers both surfaces.

## Error shape

```typescript theme={null}
interface FieldError {
  field: string;    // element-specific key, e.g. 'email', 'cardNumber'
  code: string;     // stable machine-readable code
  message: string;  // already localized, safe to render
  source?: string;  // Checkout-only: 'contact' | 'billingAddress' | 'shippingAddress' | 'paymentMethod'
}
```

## Gate submission on `complete`

The reliable signal for enabling your submit button is `onChange.complete`. It already accounts for `required`, format schemas, custom `validate` callbacks, and (for compound elements) sub-element state.

```javascript theme={null}
card.on('onChange', ({ complete }) => {
  submitBtn.disabled = !complete;
});
```

Do not infer validity from `value !== null`. Some elements emit partial typed shapes mid-typing; that is intentional and does not mean the value is valid.

## Render inline errors from `onChange.errors`

```javascript theme={null}
email.on('onChange', ({ errors }) => {
  renderInlineErrors(errors ?? []); // errors is omitted when empty
});
```

The same `FieldError[]` appears on `onError.fieldErrors` after a rejected submit. Reuse the same renderer.

## Render submit-time errors from `onError.fieldErrors`

```javascript theme={null}
card.on('onError', ({ code, message, fieldErrors, cause }) => {
  switch (code) {
    case 'validation_failed':
      paintFieldErrors(fieldErrors);
      break;
    case 'tokenization_failed':
      toast(`Card error: ${message}`);
      break;
    case 'network':
      toast('Network error. Try again.');
      break;
    default:
      toast(message);
  }
});
```

* Switch on `code` (the union is closed but new codes are added in minor releases, so keep a `default` branch).
* Render `message` as-is; it is already localized and shopper-safe.
* Do not surface `cause`; it carries raw diagnostics intended for observability.

## Layer custom rules with `validate`

To add rules on top of the built-ins, leave `validate` unset and gate your business logic elsewhere (a server-side check, or inside your own `onChange` handler). To replace the built-ins entirely, pass a `validate` callback:

```javascript theme={null}
overflow.text({
  name: 'giftMessage',
  label: 'Gift message',
  validate: (value) => {
    if (!value) return null; // optional field
    if (value.length > 140) return 'Keep it under 140 characters';
    return null;
  },
});
```

`validate` opts the element fully out of built-in checks; the callback owns the entire contract. See [`validate`](/payment-elements/options/validate).

## Bucket Checkout errors by `source`

Inside Checkout, every emitted `FieldError` carries a `source` string identifying which slot of the value envelope the field belongs to:

```javascript theme={null}
checkout.on('onChange', ({ errors }) => {
  const contactErrors = errors?.filter(e => e.source === 'contact');
  const paymentErrors = errors?.filter(e => e.source === 'paymentMethod');
  paintContactErrors(contactErrors);
  paintPaymentErrors(paymentErrors);
});
```

`source` maps 1:1 to top-level keys on `CheckoutElementValue`.

## Bank key rotation

If your server rejects an encrypted bank payload with a `UNKNOWN_KID` error (an expected condition during key rotation), call `overflow.invalidateBankFieldEncryptionCache()` and retry `bank.submit()` once. Checkout wires this automatically; standalone bank flows should mirror the pattern:

```javascript theme={null}
try {
  await postToBackend(bankValue);
} catch (err) {
  if (err.code === 'UNKNOWN_KID') {
    overflow.invalidateBankFieldEncryptionCache();
    bank.submit();
    return;
  }
  toast('Something went wrong. Try again.');
}
```

For merchant-facing messaging during outages of this kind, prefer "manual bank entry may be temporarily unavailable" rather than exposing the underlying condition.

## See also

* [`onChange`](/payment-elements/events/on-change)
* [`onError`](/payment-elements/events/on-error)
* [Error codes](/payment-elements/events/error-codes)
* [`required`](/payment-elements/options/required) · [`validate`](/payment-elements/options/validate)
