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

# Error Handling & Failure States

> Learn how to handle payment failures and display error messages in your checkout.

When implementing custom layouts, you need to handle payment failures and display appropriate error messages to your users. This guide covers the available options and best practices.

## Two types of errors

Before implementing error handling, it's important to understand the difference between validation errors and payment failures:

| Type                  | When it occurs                                            | How it's handled                                                    |
| --------------------- | --------------------------------------------------------- | ------------------------------------------------------------------- |
| **Validation errors** | During form input (invalid card number, missing fields)   | Handled automatically by input components; prevents form submission |
| **Payment failures**  | After form submission (declined card, insufficient funds) | Requires explicit handling with error container or custom code      |

## Default error behavior

<Tabs>
  <Tab title="Web">
    <Info>
      The `<primer-error-message-container>` specifically handles payment failures that occur after form submission, not card validation errors. Card validation is handled by the input components themselves and prevents form submission until valid.
    </Info>
  </Tab>

  <Tab title="Android">
    The Primer Checkout SDK handles most error scenarios automatically. Payment failures are displayed in the checkout UI, and validation errors appear inline on form fields.

    Form validation includes:

    * Card number validation (Luhn check, network detection)
    * Expiry date validation (format, future date)
    * CVV length validation (based on card network)
    * Required field validation (cardholder name, billing address)

    Errors appear when the user leaves a field (on blur), not while typing.

    When a payment fails, the SDK:

    1. Emits `PrimerCheckoutState.Failure` with error details on `checkout.state`
    2. Displays a default error screen (in `PrimerCheckoutSheet`)
    3. Offers retry and "try other methods" options
  </Tab>

  <Tab title="iOS">
    The Primer Checkout SDK handles most error scenarios automatically. Validation errors appear inline on form fields, and payment failures are displayed in the checkout UI.

    Form validation includes:

    * Card number validation (Luhn check, network detection)
    * Expiry date validation (format, future date)
    * CVV length validation (based on card network)
    * Required field validation (cardholder name, billing address)

    When a payment fails, the SDK:

    1. Reports the error via `onCompletion`
    2. Displays a default error screen
    3. Offers retry options
  </Tab>
</Tabs>

## Built-in error display

<Tabs>
  <Tab title="Web">
    The `<primer-error-message-container>` provides a convenient way to display payment failures without writing custom code:

    ```html theme={"dark"}
    <primer-checkout client-token="your-token">
      <primer-main slot="main">
        <div slot="payments">
          <primer-payment-method type="PAYMENT_CARD"></primer-payment-method>
          <primer-error-message-container></primer-error-message-container>
        </div>
      </primer-main>
    </primer-checkout>
    ```

    ```html theme={"dark"}
    <primer-checkout client-token="your-token">
      <div slot="main" id="custom-checkout">
        <div id="payment-methods">
          <primer-payment-method type="PAYMENT_CARD"></primer-payment-method>
          <primer-error-message-container></primer-error-message-container>
        </div>
      </div>
    </primer-checkout>
    ```

    For optimal user experience, place the error container:

    1. **Prominently** where it will be visible after a payment attempt
    2. **Near the action** where users will naturally look for feedback after submitting payment
    3. **In context** within the same visual area as the payment method it relates to
  </Tab>

  <Tab title="Android">
    `PrimerCheckoutSheet` displays a default error screen automatically when a payment fails. No additional code is required for basic error handling.

    To react to a failure in your own code, observe `checkout.state` and match on `PrimerCheckoutState.Failure`:

    ```kotlin theme={"dark"}
    val state by checkout.state.collectAsStateWithLifecycle()

    LaunchedEffect(state) {
        val s = state
        if (s is PrimerCheckoutState.Failure) {
            val error = s.error
            Log.e("Checkout", "Payment failed: ${error.description}")
            Log.e("Checkout", "Error code: ${error.errorCode}")
            Log.e("Checkout", "Diagnostics ID: ${error.diagnosticsId}")
        }
    }

    PrimerCheckoutSheet(checkout = checkout)
    ```
  </Tab>

  <Tab title="iOS">
    Payment failures are reported through the `onCompletion` callback. The SDK also displays a default error screen automatically:

    ```swift theme={"dark"}
    PrimerCheckout(clientToken: clientToken) { state in
      if case let .failure(error) = state {
        print("Error ID: \(error.errorId)")
        print("Description: \(error.localizedDescription)")
        print("Diagnostics ID: \(error.diagnosticsId)")
      }
    }
    ```
  </Tab>
</Tabs>

### Error properties

Every error from the SDK includes diagnostic information to help identify and resolve issues.

<Tabs>
  <Tab title="Web">
    | Property        | Description                             |
    | --------------- | --------------------------------------- |
    | `code`          | Error code identifying the failure type |
    | `message`       | Human-readable error message            |
    | `diagnosticsId` | Reference ID for Primer support         |
  </Tab>

  <Tab title="Android">
    | Property             | Type      | Description                          |
    | -------------------- | --------- | ------------------------------------ |
    | `errorId`            | `String`  | Unique error identifier              |
    | `description`        | `String`  | Human-readable error message         |
    | `errorCode`          | `String?` | Error code (e.g., `"card_declined"`) |
    | `diagnosticsId`      | `String`  | Reference ID for support             |
    | `recoverySuggestion` | `String?` | Suggested recovery action            |
  </Tab>

  <Tab title="iOS">
    | Property             | Type      | Description                                                                           |
    | -------------------- | --------- | ------------------------------------------------------------------------------------- |
    | `errorId`            | `String`  | Stable, kebab-case identifier for the error category (use for programmatic branching) |
    | `diagnosticsId`      | `String`  | Unique per-occurrence diagnostics ID to share with Primer support                     |
    | `recoverySuggestion` | `String?` | Suggested recovery action, when one applies                                           |
    | `errorDescription`   | `String?` | Human-readable description for logging (also available as `localizedDescription`)     |
  </Tab>
</Tabs>

## Custom error handling

<Tabs>
  <Tab title="Web">
    You can implement your own payment failure handling using the SDK callbacks and events. Choose **one** of the following approaches:

    <Tabs>
      <Tab title="Callbacks (primer:ready)">
        ```javascript theme={"dark"}
        const checkout = document.querySelector('primer-checkout');

        checkout.addEventListener('primer:ready', (event) => {
          const primer = event.detail;

          primer.onPaymentSuccess = ({ payment, paymentMethodType }) => {
            document.getElementById('my-custom-error').style.display = 'none';
            window.location.href = `/order/confirmation?id=${payment.orderId}`;
          };

          primer.onPaymentFailure = ({ error, payment, paymentMethodType }) => {
            const customErrorElement = document.getElementById('my-custom-error');
            customErrorElement.textContent = error.message;
            customErrorElement.style.display = 'block';

            console.error('Payment failed:', {
              code: error.code,
              message: error.message,
              diagnosticsId: error.diagnosticsId,
            });
          };
        });
        ```
      </Tab>

      <Tab title="State change event">
        ```javascript theme={"dark"}
        const checkout = document.querySelector('primer-checkout');

        checkout.addEventListener('primer:state-change', (event) => {
          const { primerJsError, paymentFailure } = event.detail;

          if (primerJsError || paymentFailure) {
            const customErrorElement = document.getElementById('my-custom-error');
            const errorMessage = primerJsError?.message || paymentFailure?.message;
            customErrorElement.textContent = errorMessage;
            customErrorElement.style.display = 'block';
          } else {
            document.getElementById('my-custom-error').style.display = 'none';
          }
        });
        ```
      </Tab>
    </Tabs>

    This approach gives you complete control over payment failure presentation but requires you to implement the error handling logic yourself.
  </Tab>

  <Tab title="Android">
    Override the default error screen in `PrimerCheckoutSheet` with a custom Composable:

    ```kotlin theme={"dark"}
    PrimerCheckoutSheet(
        checkout = checkout,
        error = { error ->
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(24.dp),
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {
                Icon(
                    imageVector = Icons.Default.Warning,
                    contentDescription = null,
                    tint = MaterialTheme.colorScheme.error,
                    modifier = Modifier.size(48.dp),
                )
                Spacer(Modifier.height(16.dp))
                Text(
                    text = "Payment failed",
                    style = MaterialTheme.typography.titleLarge,
                )
                Spacer(Modifier.height(8.dp))
                Text(
                    text = error.description,
                    style = MaterialTheme.typography.bodyMedium,
                    textAlign = TextAlign.Center,
                )
                Spacer(Modifier.height(24.dp))
                Button(onClick = { checkout.dismiss() }) {
                    Text("Try again")
                }
            }
        },
    )
    ```

    ### Observing validation state

    For inline components, observe card form validation directly:

    ```kotlin theme={"dark"}
    val cardFormController = rememberCardFormController(checkout)
    val formState by cardFormController.state.collectAsStateWithLifecycle()

    val errors = formState.fieldErrors
    errors?.forEach { error ->
        Log.d("Validation", "${error.inputElementType}: ${error.errorId}")
    }
    ```
  </Tab>

  <Tab title="iOS">
    `PrimerCheckout` renders the managed error screen and does not expose screen slots. For full control over how a payment failure is presented, build your own layout from the composable views, drive it with a [`PrimerCheckoutSession`](/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-session), and handle the terminal `.failure` state in the `.primerCheckoutSession(_:onCompletion:)` completion closure:

    ```swift theme={"dark"}
    struct CheckoutView: View {
      @StateObject private var session: PrimerCheckoutSession
      @State private var errorMessage: String?

      init(clientToken: String) {
        _session = StateObject(wrappedValue: PrimerCheckoutSession(clientToken: clientToken))
      }

      var body: some View {
        ScrollView {
          PrimerCardForm()

          if let errorMessage {
            VStack(spacing: 20) {
              Image(systemName: "xmark.circle.fill")
                .font(.system(size: 48))
                .foregroundColor(.red)
              Text("Something went wrong")
                .font(.title3.weight(.semibold))
              Text(errorMessage)
                .font(.body)
                .foregroundColor(.secondary)
                .multilineTextAlignment(.center)
            }
            .padding()
          }
        }
        .primerCheckoutSession(session) { state in
          if case let .failure(error) = state {
            errorMessage = error.recoverySuggestion ?? "Please try again."
            print("Checkout failed [\(error.errorId)] — diagnosticsId: \(error.diagnosticsId)")
          }
        }
      }
    }
    ```

    For UIKit integrations, the same `PrimerError` arrives through [`PrimerCheckoutPresenterDelegate`](/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-presenter)'s `primerCheckoutPresenterDidFailWithError(_:)` callback.

    ### Observing validation state

    The card form is driven by [`PrimerCardFormSession`](/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/card-form-session), which publishes `PrimerCardFormState`. Observe it inside a [`PrimerCardForm`](/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/primer-card-form) slot to read validation errors per field:

    ```swift theme={"dark"}
    PrimerCardForm(
      cardDetails: { session in
        VStack(alignment: .leading) {
          CardFormDefaults.cardNumber(session)
          if session.state.hasError(for: .cardNumber),
             let message = session.state.errorMessage(for: .cardNumber) {
            Text(message).foregroundColor(.red)
          }
        }
      }
    )
    ```

    To inspect every current field error, iterate `state.fieldErrors`:

    ```swift theme={"dark"}
    ForEach(session.state.fieldErrors) { error in
      Text("\(error.fieldType): \(error.message)")
    }
    ```

    Field presence is Dashboard-driven through `CardFormConfiguration`, and validation runs automatically as the shopper edits each field — gate your submit button on `session.state.isValid`.
  </Tab>
</Tabs>

## Choosing the right approach

| Approach               | Best for                                 | Trade-offs                 |
| ---------------------- | ---------------------------------------- | -------------------------- |
| Built-in error display | Quick implementation, consistent styling | Less customization control |
| Custom error handling  | Full control over UX and design          | More code to maintain      |

## Best practices

<Tip>
  1. **Always handle errors** - Never leave users without feedback after a failed payment
  2. **Be specific** - Show meaningful error messages that help users understand what went wrong
  3. **Provide next steps** - Guide users on how to resolve the issue (try another card, check details, etc.)
  4. **Clear on retry** - Hide error messages when users attempt a new payment
  5. **Log for debugging** - Capture error details including `diagnosticsId` for troubleshooting
</Tip>

## See also

<CardGroup cols={2}>
  <Card title="Error message container" icon="cube" href="/docs/sdk/primer-checkout-web/components/primer-error-message-container">
    Web component SDK reference
  </Card>

  <Card title="Android SDK reference" icon="android" href="/docs/sdk/android-checkout/v3.0.0-beta/api/overview">
    Android SDK API documentation
  </Card>

  <Card title="Events guide" icon="bolt" href="/docs/checkout/primer-checkout/configuration/events">
    Complete reference for all checkout events
  </Card>

  <Card title="Log Errors Guide" href="/docs/checkout/primer-checkout/guides-and-recipes/log-errors-debugging">
    Capture and log payment errors for debugging
  </Card>
</CardGroup>
