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.
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
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.
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:
Fires PrimerCheckoutEvent.Failure with error details
Displays a default error screen (in PrimerCheckoutSheet)
Offers retry and “try other methods” options
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:
Reports the error via onCompletion
Displays a default error screen
Offers retry options
Built-in error display
The <primer-error-message-container> provides a convenient way to display payment failures without writing custom code: < 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 >
< 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:
Prominently where it will be visible after a payment attempt
Near the action where users will naturally look for feedback after submitting payment
In context within the same visual area as the payment method it relates to
PrimerCheckoutSheet displays a default error screen automatically when a payment fails. No additional code is required for basic error handling: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 } " )
}
}
},
)
Payment failures are reported through the onCompletion callback. The SDK also displays a default error screen automatically: 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 ?? "" ) " )
}
}
)
Error properties
Every error from the SDK includes diagnostic information to help identify and resolve issues.
Property Description codeError code identifying the failure type messageHuman-readable error message diagnosticsIdReference ID for Primer support
Property Type Description errorIdStringUnique error identifier descriptionStringHuman-readable error message errorCodeString?Error code (e.g., "card_declined") diagnosticsIdStringReference ID for support recoverySuggestionString?Suggested recovery action
Property Type Description errorIdStringUnique error identifier for support requests errorDescriptionString?Human-readable error message diagnosticsIdString?Diagnostic ID for Primer support recoverySuggestionString?Suggested recovery action
Custom error handling
You can implement your own payment failure handling using the SDK callbacks and events. Choose one of the following approaches: Callbacks (primer:ready)
State change event
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 ,
});
};
});
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' ;
}
});
This approach gives you complete control over payment failure presentation but requires you to implement the error handling logic yourself. Override the default error screen in PrimerCheckoutSheet with a custom Composable: 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: val cardFormController = rememberCardFormController (checkout)
val formState by cardFormController.state. collectAsStateWithLifecycle ()
val errors = formState.fieldErrors
errors?. forEach { error ->
Log. d ( "Validation" , " ${ error.inputElementType } : ${ error.errorId } " )
}
Replace the default error screen with your own SwiftUI view: 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: 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: cardFormScope. setFieldError (. email , message : "Please use a valid email" , errorCode : nil )
cardFormScope. clearFieldError (. email )
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
Always handle errors - Never leave users without feedback after a failed payment
Be specific - Show meaningful error messages that help users understand what went wrong
Provide next steps - Guide users on how to resolve the issue (try another card, check details, etc.)
Clear on retry - Hide error messages when users attempt a new payment
Log for debugging - Capture error details including diagnosticsId for troubleshooting
See also
Error message container Web component SDK reference
Android SDK reference Android SDK API documentation
Events guide Complete reference for all checkout events
Log Errors Guide Capture and log payment errors for debugging