> ## 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. Fires `PrimerCheckoutEvent.Failure` with error details
    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:

    ```kotlin theme={"dark"}
    PrimerCheckoutSheet(
        checkout = checkout,
        onEvent = { event ->
            when (event) {
                is PrimerCheckoutEvent.Failure -> {
                    val error = event.error
                    Log.e("Checkout", "Payment failed: ${error.description}")
                    Log.e("Checkout", "Error code: ${error.errorCode}")
                    Log.e("Checkout", "Diagnostics ID: ${error.diagnosticsId}")
                }
            }
        },
    )
    ```
  </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,
      onCompletion: { state in
        if case .failure(let error) = state {
          print("Error ID: \(error.errorId)")
          print("Description: \(error.errorDescription ?? "")")
          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`  | Unique error identifier for support requests |
    | `errorDescription`   | `String?` | Human-readable error message                 |
    | `diagnosticsId`      | `String?` | Diagnostic ID for Primer support             |
    | `recoverySuggestion` | `String?` | Suggested recovery action                    |
  </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">
    Replace the default error screen with your own SwiftUI view:

    ```swift theme={"dark"}
    checkoutScope.errorScreen = { errorMessage in
      AnyView(
        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)
          Button("Try again") { }
            .buttonStyle(.borderedProminent)
        }
        .padding()
      )
    }
    ```

    ### Observing validation state

    Observe card form validation errors via `AsyncStream`:

    ```swift theme={"dark"}
    Task {
      for await state in cardFormScope.state {
        for error in state.fieldErrors {
          print("\(error.fieldType): \(error.message)")
        }
      }
    }
    ```

    You can also set custom validation errors programmatically:

    ```swift theme={"dark"}
    cardFormScope.setFieldError(.email, message: "Please use a valid email", errorCode: nil)
    cardFormScope.clearFieldError(.email)
    ```
  </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="/sdk/primer-checkout-web/components/primer-error-message-container">
    Web component SDK reference
  </Card>

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

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

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