> ## 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.

# Log errors for debugging

> Capture and log payment errors for debugging and support.

Log payment errors with diagnostic information to help debug issues and work with Primer support.

## Recipe

> [tab] Web
>
> ```javascript theme={"dark"}
> checkout.addEventListener('primer:ready', (event) => {
>   const primer = event.detail;
>
>   primer.onPaymentFailure = ({ error }) => {
>     console.error('Payment failed:', {
>       code: error.code,
>       message: error.message,
>       diagnosticsId: error.diagnosticsId, // Share with Primer support
>     });
>   };
> });
> ```

> [tab] Android
>
> ```kotlin theme={"dark"}
> val state by checkout.state.collectAsStateWithLifecycle()
> LaunchedEffect(state) {
>     when (val s = state) {
>         is PrimerCheckoutState.Failure -> {
>             val error = s.primerError
>             Log.e("Checkout", "Payment failed: ${error.description}")
>             Log.e("Checkout", "Error code: ${error.errorCode}")
>             Log.e("Checkout", "Diagnostics ID: ${error.diagnosticsId}")
>             Log.e("Checkout", "Recovery: ${error.recoverySuggestion}")
>         }
>         else -> Unit
>     }
> }
>
> PrimerCheckoutSheet(checkout = checkout)
> ```

> [tab] iOS
>
> ```swift theme={"dark"}
> PrimerCheckout(
>   clientToken: clientToken,
>   onCompletion: { state in
>     if case let .failure(error, checkoutData) = state {
>       print("[Primer] Error ID: \(error.errorId)")
>       print("[Primer] Description: \(error.errorDescription ?? "N/A")")
>       print("[Primer] Diagnostics: \(error.diagnosticsId)")
>       print("[Primer] Recovery: \(error.recoverySuggestion ?? "N/A")")
>       print("[Primer] Payment ID: \(checkoutData?.payment?.id ?? "none")")
>       print("[Primer] Order ID: \(checkoutData?.payment?.orderId ?? "none")")
>     }
>   }
> )
> ```

## How it works

> [tab] Web
>
> 1. Listen for the `primer:ready` event to access the Primer SDK instance
> 2. Set the `onPaymentFailure` callback
> 3. Log the error details including `diagnosticsId` which helps Primer support investigate issues

> [tab] Android
>
> 1. Observe `checkout.state` with `collectAsStateWithLifecycle()` and react in a `LaunchedEffect`
> 2. Handle `PrimerCheckoutState.Failure` to access the `PrimerError`
> 3. Log the error details including `diagnosticsId` which helps Primer support investigate issues
>
> Every `PrimerError` includes these properties:
>
> | 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 Primer support |
> | `recoverySuggestion` | `String?` | Suggested recovery action for the user |

> [tab] iOS
>
> 1. Receive `PrimerCheckoutState` through the `onCompletion` closure on `PrimerCheckout` or the `.primerCheckoutSession(_:theme:onCompletion:)` modifier, and match `.failure`
> 2. Access `PrimerError` properties for debugging details
> 3. Log the `diagnosticsId` which helps Primer support investigate issues
>
> Every `PrimerError` includes these properties:
>
> | Property | Type | Description |
> | - | - | - |
> | `errorId` | `String` | Stable identifier for the error category |
> | `errorDescription` | `String?` | Human-readable error message for logging |
> | `diagnosticsId` | `String` | Reference ID for Primer support |
> | `recoverySuggestion` | `String?` | Developer-facing hint on how to fix the cause. `nil` for most payment failures, such as `payment-failed` |
>
> `.failure` also carries `checkoutData`. When the payment was created before it failed (for example a decline), `checkoutData?.payment` holds the payment ID and order ID. Log them so you can look up the payment on your backend. `checkoutData` is `nil` when the failure happened before a payment existed (initialization, tokenization, connectivity).
>
> `.failure` arrives once per failed payment attempt. With the SDK error screen on (the default), the checkout stays open for a retry. Don't treat `.failure` as the end of the session. `.success` or `.dismissed` arrives once and ends the session.
>
> If loading fails, `PrimerCheckout` delivers `.failure`. With the SDK error screen on (the default), the checkout stays open for a retry. With `isErrorScreenEnabled: false`, the checkout shows nothing and you build your own retry UI. The `.primerCheckoutSession` modifier delivers a load failure once and ends the session.

> [!TIP]
>
> The `diagnosticsId` is a unique identifier for the error. When contacting Primer support, always include this ID to help them quickly locate and diagnose the issue.

## Variations

### Send errors to logging service

> [tab] Web
>
> ```javascript theme={"dark"}
> primer.onPaymentFailure = ({ error, paymentMethodType }) => {
>   // Send to your logging service (Sentry, LogRocket, etc.)
>   Sentry.captureException(new Error(error.message), {
>     extra: {
>       code: error.code,
>       diagnosticsId: error.diagnosticsId,
>       paymentMethod: paymentMethodType,
>     },
>   });
> };
> ```

> [tab] Android
>
> ```kotlin theme={"dark"}
> is PrimerCheckoutState.Failure -> {
>     val error = s.primerError
>     Sentry.captureException(Exception(error.description)) { scope ->
>         scope.setExtra("error_code", error.errorCode.orEmpty())
>         scope.setExtra("diagnostics_id", error.diagnosticsId)
>     }
> }
> ```

> [tab] iOS
>
> ```swift theme={"dark"}
> if case let .failure(error, _) = state {
>   ErrorLogger.log(
>     errorId: error.errorId,
>     description: error.errorDescription ?? "Unknown error",
>     diagnosticsId: error.diagnosticsId
>   )
> }
> ```

### Custom error display

> [tab] Web
>
> ```javascript theme={"dark"}
> const primerEvents = [
>   'primer:ready',
>   'primer:state-change',
>   'primer:methods-update',
>   'primer:payment-start',
>   'primer:payment-success',
>   'primer:payment-failure',
>   'primer:card-success',
>   'primer:card-error',
>   'primer:bin-data-available',
>   'primer:vault-methods-update',
> ];
>
> primerEvents.forEach((eventName) => {
>   document.addEventListener(eventName, (event) => {
>     console.log(`[${eventName}]`, event.detail);
>   });
> });
> ```

> [tab] Android
>
> Override the error slot in `PrimerCheckoutSheet` to display a custom error screen. Observe `checkout.state` separately to log diagnostics:
>
> ```kotlin theme={"dark"}
> val state by checkout.state.collectAsStateWithLifecycle()
> LaunchedEffect(state) {
>     when (val s = state) {
>         is PrimerCheckoutState.Failure -> {
>             Log.e("Checkout", "Diagnostics: ${s.primerError.diagnosticsId}")
>         }
>         else -> Unit
>     }
> }
>
> PrimerCheckoutSheet(
>     checkout = checkout,
>     error = { primerError, _ ->
>         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 = "Something went wrong",
>                 style = MaterialTheme.typography.titleLarge,
>             )
>             Spacer(Modifier.height(8.dp))
>             Text(
>                 text = primerError.description,
>                 style = MaterialTheme.typography.bodyMedium,
>                 textAlign = TextAlign.Center,
>                 color = MaterialTheme.colorScheme.onSurfaceVariant,
>             )
>             if (primerError.recoverySuggestion != null) {
>                 Spacer(Modifier.height(4.dp))
>                 Text(
>                     text = primerError.recoverySuggestion!!,
>                     style = MaterialTheme.typography.bodySmall,
>                     color = MaterialTheme.colorScheme.onSurfaceVariant,
>                     textAlign = TextAlign.Center,
>                 )
>             }
>             Spacer(Modifier.height(24.dp))
>             Button(
>                 onClick = { checkout.refresh() },
>                 modifier = Modifier.fillMaxWidth(),
>             ) {
>                 Text("Try again")
>             }
>             Spacer(Modifier.height(8.dp))
>             OutlinedButton(
>                 onClick = { checkout.dismiss() },
>                 modifier = Modifier.fillMaxWidth(),
>             ) {
>                 Text("Use a different method")
>             }
>         }
>     },
>     onDismiss = { },
> )
> ```

> [tab] iOS
>
> Compose the checkout with `PrimerCheckoutSession`. Turn off the SDK error screen with `PrimerUIOptions(isErrorScreenEnabled: false)`. Then use each `.failure` from the `.primerCheckoutSession` modifier's `onCompletion` to drive your own error screen. If you leave the SDK error screen on (the default), it opens in a sheet over your view on every failed payment. Your error view appears only after the shopper dismisses that sheet.
>
> ```swift theme={"dark"}
> struct CheckoutView: View {
>   @StateObject private var session: PrimerCheckoutSession
>   @State private var paymentError: PrimerError?
>
>   init(clientToken: String) {
>     let settings = PrimerSettings(uiOptions: PrimerUIOptions(isErrorScreenEnabled: false))
>     _session = StateObject(wrappedValue: PrimerCheckoutSession(clientToken: clientToken, settings: settings))
>   }
>
>   var body: some View {
>     ScrollView {
>       if paymentError != nil {
>         VStack(spacing: 16) {
>           Image(systemName: "exclamationmark.triangle.fill")
>             .font(.largeTitle)
>             .foregroundColor(.red)
>           Text("Something went wrong")
>             .font(.title2)
>           Text("Please try a different payment method.")
>             .multilineTextAlignment(.center)
>             .foregroundColor(.secondary)
>           Button("Try again") {
>             paymentError = nil
>             Task {
>               if session.phase == .ready {
>                 await session.refresh()
>               } else {
>                 session.cancel()
>                 await session.start()
>               }
>             }
>           }
>           .buttonStyle(.borderedProminent)
>         }
>         .padding(24)
>       } else {
>         PrimerPaymentMethods()
>         PrimerCardForm()
>       }
>     }
>     .primerCheckoutSession(session) { state in
>       if case let .failure(error, checkoutData) = state {
>         print("[Primer] Error: \(error.errorDescription ?? "N/A")")
>         print("[Primer] Diagnostics: \(error.diagnosticsId)")
>         print("[Primer] Payment ID: \(checkoutData?.payment?.id ?? "none")")
>         paymentError = error
>       }
>     }
>   }
> }
> ```
>
> `refresh()` only reloads a session that has finished loading. If loading failed, "Try again" calls `cancel()` and then `start()` to rebuild the session.

### Inline error handling

> [tab] Web
>
> ```javascript theme={"dark"}
> checkout.addEventListener('primer:methods-update', (event) => {
>   const methods = event.detail.toArray();
>   console.table(methods.map((m) => ({ type: m.type, id: m.id })));
> });
> ```

> [tab] Android
>
> For `PrimerCheckoutHost`, observe `checkout.state` and display your own UI:
>
> ```kotlin theme={"dark"}
> @Composable
> fun InlineCheckoutWithErrors(clientToken: String) {
>     val checkout = rememberPrimerCheckoutController(clientToken)
>     val state by checkout.state.collectAsStateWithLifecycle()
>     var paymentError by remember { mutableStateOf<PrimerError?>(null) }
>
>     LaunchedEffect(state) {
>         when (val s = state) {
>             is PrimerCheckoutState.Failure -> {
>                 paymentError = s.primerError
>             }
>             is PrimerCheckoutState.Success -> {
>                 paymentError = null
>             }
>             else -> Unit
>         }
>     }
>
>     when (state) {
>         is PrimerCheckoutState.Loading -> {
>             CircularProgressIndicator()
>         }
>         is PrimerCheckoutState.Ready -> {
>             PrimerCheckoutHost(checkout = checkout) {
>                 val cardFormController = rememberCardFormController(checkout)
>
>                 Column(Modifier.padding(16.dp)) {
>                     PrimerCardForm(controller = cardFormController)
>
>                     paymentError?.let { error ->
>                         Spacer(Modifier.height(16.dp))
>                         Card(
>                             colors = CardDefaults.cardColors(
>                                 containerColor = MaterialTheme.colorScheme.errorContainer,
>                             ),
>                             modifier = Modifier.fillMaxWidth(),
>                         ) {
>                             Row(
>                                 modifier = Modifier.padding(16.dp),
>                                 verticalAlignment = Alignment.CenterVertically,
>                             ) {
>                                 Icon(
>                                     imageVector = Icons.Default.Warning,
>                                     contentDescription = null,
>                                     tint = MaterialTheme.colorScheme.error,
>                                 )
>                                 Spacer(Modifier.width(12.dp))
>                                 Column(Modifier.weight(1f)) {
>                                     Text(
>                                         text = error.description,
>                                         style = MaterialTheme.typography.bodyMedium,
>                                     )
>                                     if (error.recoverySuggestion != null) {
>                                         Text(
>                                             text = error.recoverySuggestion!!,
>                                             style = MaterialTheme.typography.bodySmall,
>                                         )
>                                     }
>                                 }
>                             }
>                         }
>                     }
>                 }
>             }
>         }
>     }
> }
> ```

> [tab] iOS
>
> Read per-field validation errors from `PrimerCardFormSession.state.fieldErrors` inside a `PrimerCardForm` slot, and handle each failed payment attempt in `onCompletion`:
>
> ```swift theme={"dark"}
> struct InlineCheckoutWithErrors: View {
>   @StateObject private var session: PrimerCheckoutSession
>
>   init(clientToken: String) {
>     _session = StateObject(wrappedValue: PrimerCheckoutSession(clientToken: clientToken))
>   }
>
>   var body: some View {
>     ScrollView {
>       PrimerCardForm(
>         cardDetails: { cardSession in
>           CardDetails(session: cardSession)
>         }
>       )
>       .padding()
>     }
>     .primerCheckoutSession(session) { state in
>       if case let .failure(error, _) = state {
>         print("[Primer] Payment error: \(error.errorDescription ?? "N/A")")
>       }
>     }
>   }
> }
>
> struct CardDetails: View {
>   @ObservedObject var session: PrimerCardFormSession
>
>   var body: some View {
>     VStack(alignment: .leading, spacing: 8) {
>       CardFormDefaults.cardDetails(session)
>       ForEach(session.state.fieldErrors) { fieldError in
>         Text("\(fieldError.fieldType): \(fieldError.message)")
>           .font(.caption)
>           .foregroundColor(.red)
>       }
>     }
>   }
> }
> ```

## See also

> [card] [Track payment in analytics](https://primer.io/docs/checkout/primer-checkout/guides-and-recipes/track-payment-analytics.md)
>
> Send payment events to your analytics platform

> [card] [Error handling](https://primer.io/docs/checkout/primer-checkout/build-your-ui/error-handling.md)
>
> Handle payment failures and display error messages


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.