Async Stripe

Error Handling

Understanding and handling Stripe API errors effectively

Stripe API requests can fail for various reasons: invalid parameters, authentication issues, card declines, rate limits, and network failures. The StripeError enum provides structured error information to help you handle these cases appropriately.

Error Types

The StripeError enum has several variants:

/// An error encountered when communicating with the Stripe API.
#[derive(Debug, Error)]
pub enum StripeError {
    /// Stripe returned a client error.
    #[error("error reported by stripe: {0:#?}, status code: {1}")]
    Stripe(Box<ApiErrors>, u16),
    /// An error occurred when parsing the Stripe response.
    #[error("error deserializing a request: {0}")]
    JSONDeserialize(String),
    /// An error occurred communicating with Stripe.
    #[error("error communicating with stripe: {0}")]
    ClientError(String),
    /// The client configuration was invalid.
    #[error("configuration error: {0}")]
    ConfigError(String),
    /// A blocking request timed out
    #[error("timeout communicating with stripe")]
    Timeout,
}

Handling API Errors

The most common error type is StripeError::Stripe, which contains the error details from Stripe's API along with the HTTP status code.

Basic Error Handling

match Customer::create(&client, CreateCustomer::new()).send().await {
    Ok(customer) => info!("Created customer: {}", customer.id),
    Err(err) => match err {
        StripeError::Stripe(api_error, status_code) => {
            error!("Stripe API error ({}): {:?}", status_code, api_error.message);
        }
        StripeError::ClientError(msg) => {
            error!("Network error: {}", msg);
        }
        _ => {
            error!("Other error: {}", err);
        }
    },
}

Handling Specific HTTP Status Codes

Different status codes indicate different types of failures:

    match PaymentIntent::retrieve(&client, &payment_intent_id, &[]).await {
        Ok(payment) => {
            info!("Payment status: {:?}", payment.status);
        }
        Err(StripeError::Stripe(api_error, status)) => match status {
            400 => {
                // Bad Request - Invalid parameters
                error!("Invalid request: {:?}", api_error.message);
            }
            401 => {
                // Unauthorized - Invalid API key
                error!("Authentication failed - check your API key");
            }
            402 => {
                // Payment Required - Card declined or insufficient funds
                error!("Payment failed: {:?}", api_error.message);

                // The error may contain a decline code
                if let Some(code) = &api_error.code {
                    match code.as_str() {
                        "card_declined" => error!("Card was declined"),
                        "insufficient_funds" => error!("Insufficient funds"),
                        "expired_card" => error!("Card has expired"),
                        _ => error!("Payment error code: {}", code),
                    }
                }
            }
            404 => {
                // Not Found - Resource doesn't exist
                error!("Resource not found");
            }
            429 => {
                // Too Many Requests - Rate limited
                warn!("Rate limited - slow down requests");
            }
            500 | 502 | 503 | 504 => {
                // Server errors - retry with backoff
                error!("Stripe server error - retry later");
            }
            _ => {
                error!("HTTP {}: {:?}", status, api_error.message);
            }
        },
        Err(e) => {
            error!("Non-API error: {}", e);
        }
    }
}

Common Error Codes

Stripe includes error codes in the ApiErrors struct that provide more specific information about what went wrong:

Payment Errors (402)

  • card_declined - The card was declined
  • expired_card - The card has expired
  • incorrect_cvc - The CVC is incorrect
  • processing_error - An error occurred while processing the card
  • insufficient_funds - Insufficient funds in the account

Request Errors (400)

  • parameter_invalid_empty - A required parameter was empty
  • parameter_unknown - An unknown parameter was provided
  • resource_missing - The requested resource doesn't exist

Authentication Errors (401)

  • invalid_api_key - The API key is invalid

For a complete list of error codes, see the Stripe Error Codes documentation.

Parsing Errors

By default, async-stripe uses miniserde for deserializing API responses. This significantly reduces compile times and binary size, but provides minimal error messages when deserialization fails.

Understanding Deserialization Failures

If you receive a deserialization error, it may look like:

Error: failed to deserialize response

This typically means the JSON response from Stripe doesn't match the expected structure. This can happen when:

  • Stripe adds new fields to their API
  • You're using an outdated version of async-stripe
  • The response contains unexpected values

Getting Better Error Messages

For detailed diagnostics about which field failed and why, enable the deserialize feature to use serde instead:

[dependencies]
stripe-core = { version = "1.0.0-alpha.8", features = ["customer", "deserialize"] }

This provides comprehensive error context with serde_path_to_error, showing exactly where in the JSON structure the error occurred at the cost of significantly increased compile times, link times, and binary size. For context, the parser below needs 14MB just to parse webhook data.

Why Not serde_json?

The compile-time and binary size impact of serde_json is substantial due to monomorphization. Each generic serde function gets compiled separately for every type, leading to code bloat. With hundreds of Stripe types, this results in massive binaries and slow compile times, since stripe needs to generate X00,000 lines of code to define how to deserialize each type. miniserde avoids this by using trait objects instead of generics, dramatically reducing the amount of generated code. For a deep dive into this topic, see The Dark Side of Inlining and Monomorphization.

Testing Event Parsing

You can test how async-stripe parses Stripe events using the interactive parser below. This uses the actual async-stripe parser compiled to WebAssembly with serde_path_to_error, which reports exactly where deserialization failed:

See the Performance documentation for more details on the hybrid serialization strategy and when to use the deserialize feature.

Retry Strategies

For transient errors (network issues, server errors), use the built-in retry strategies:

let client = Client::builder(secret_key)
    .request_strategy(RequestStrategy::ExponentialBackoff(3))
    .build();

// Replace with an actual customer ID for testing
let customer_id = "cus_example";

// This request will automatically retry up to 3 times with backoff
let customer = Customer::retrieve(&client, &customer_id, &[]).await;

match customer {
    Ok(customer) => info!("Retrieved customer: {}", customer.id),
    Err(e) => error!("Failed to retrieve customer after retries: {}", e),
}

See the Request Strategies documentation for more details on retry behavior.

Best Practices

1. Handle Specific Errors

Don't just log all errors the same way. Handle payment failures differently from configuration errors:

match result {
    Err(StripeError::Stripe(api_error, 402)) => {
        // Show user-friendly message for payment failures
        show_payment_error_to_user(&api_error);
    }
    Err(StripeError::Stripe(api_error, 400)) => {
        // Log parameter errors for debugging
        error!("Invalid parameters: {:?}", api_error);
    }
    Err(e) => {
        // Log unexpected errors and alert monitoring
        error!("Unexpected Stripe error: {}", e);
    }
    Ok(_result) => {
        // Success
        info!("Operation succeeded");
    }
}

2. Use Idempotency Keys

For critical operations (especially payment creation), always use idempotency keys to prevent accidental duplicate charges:

let key = IdempotencyKey::new("order_12345").unwrap();

let payment = CreatePaymentIntent::new(1000, Currency::USD)
    .request_strategy(RequestStrategy::Idempotent(key))
    .send(client)
    .await;

match payment {
    Ok(payment_intent) => {
        info!("Created payment intent: {}", payment_intent.id);
    }
    Err(e) => {
        error!("Failed to create payment: {}", e);
    }
}

3. Log Error Details

The ApiErrors struct contains useful debugging information:

let result = PaymentIntent::retrieve(client, "pi_example", &[]).await;

if let Err(StripeError::Stripe(api_error, status)) = result {
    error!(
        status = status,
        error_type = ?api_error.error_type,
        code = ?api_error.code,
        message = ?api_error.message,
        param = ?api_error.param,
        "Stripe error"
    );
}

4. Don't Retry Client Errors

4xx errors (except 429 rate limits) usually indicate a problem with your request that won't be fixed by retrying. Only retry 5xx server errors and network failures.

The built-in RequestStrategy::ExponentialBackoff handles this correctly for you.

Never retry failed payments without user confirmation. A failed payment could be intentional (e.g., user canceled) or indicate fraud prevention.

Have feedback? Let us know here