Error Handling
Learn how to handle errors gracefully when working with the PayPal API.
Overview
Payper provides structured error handling through:
- Typed exceptions -
PayperExceptionfor API errors - HTTP status codes - Access response status codes
- Error details - Extract error messages and details from responses
- Response inspection - Access raw HTTP responses
Types of Errors
1. Authentication Errors
Invalid credentials, expired tokens:
// Use toResponse() to check status codes
var response = client.products()
.create()
.withBody(productRequest)
.retrieve()
.toResponse();
if (response.statusCode() == 401) {
System.err.println("Authentication failed");
System.err.println("Check your Client ID and Secret");
// Access error details
var errorEntity = response.toErrorEntity();
System.err.println("Error: " + errorEntity.message());
} else if (response.isSuccessful()) {
var product = response.toEntity();
System.out.println("Product created: " + product.id());
} else {
System.err.println("Unexpected error: " + response.statusCode());
}
Common causes: - Wrong Client ID or Secret - Expired credentials - Using sandbox credentials with live URL (or vice versa) - Missing required OAuth scopes
2. Validation Errors
Invalid request data:
var response = client.orders()
.create()
.withBody(orderRequest)
.retrieve()
.toResponse();
if (!response.isSuccessful()) {
System.err.println("Request failed with status: " + response.statusCode());
if (response.statusCode() == 400) {
System.err.println("Invalid request data");
var errorEntity = response.toErrorEntity();
System.err.println("Error details: " + errorEntity.message());
}
} else {
var order = response.toEntity();
System.out.println("Order created: " + order.id());
}
Common validation errors: - Missing required fields - Invalid field values - Invalid currency codes - Malformed data
3. Business Logic Errors
PayPal business rule violations:
var response = client.orders()
.capture()
.withId(orderId)
.withBody(captureRequest)
.retrieve()
.toResponse();
switch (response.statusCode()) {
case 200:
case 201:
var order = response.toEntity();
System.out.println("Order captured: " + order.id());
break;
case 422:
System.err.println("Cannot process request");
var errorEntity = response.toErrorEntity();
System.err.println("Reason: " + errorEntity.message());
// Order may already be captured, cancelled, or expired
break;
case 404:
System.err.println("Order not found");
// Order ID may be invalid or expired
break;
default:
System.err.println("Unexpected error: " + response.statusCode());
}
Common business errors: - Order already captured - Order expired - Insufficient funds - Payment declined - Subscription already cancelled
Exception Hierarchy
PayperException is a runtime exception that provides:
getMessage()- Error messagegetCause()- Underlying cause (e.g., network error)
For accessing HTTP status codes and response details, use PayperResponse directly by calling toResponse() or toFuture() instead of toEntity().
Handling Errors
Basic Error Handling
import io.github.eealba.payper.core.client.PayperResponse;
// Get response object to check status
var response = client.orders()
.get()
.withId(orderId)
.retrieve()
.toResponse();
if (response.isSuccessful()) {
var order = response.toEntity();
System.out.println("Order: " + order.id());
} else {
System.err.println("Error: Status " + response.statusCode());
// Access error details
var errorEntity = response.toErrorEntity();
System.err.println("Message: " + errorEntity.message());
}
Inspecting HTTP Status Codes
Use PayperResponse to access status codes:
import io.github.eealba.payper.core.client.PayperResponse;
try {
PayperResponse<Product, ?> response = client.products()
.create()
.withBody(productRequest)
.retrieve()
.toResponse();
switch (response.statusCode()) {
case 201:
Product product = response.toEntity();
System.out.println("Product created: " + product.id());
break;
case 400:
System.err.println("Bad Request - Invalid data");
break;
case 401:
System.err.println("Unauthorized - Check credentials");
break;
case 403:
System.err.println("Forbidden - Insufficient permissions");
break;
case 404:
System.err.println("Not Found - Resource doesn't exist");
break;
case 422:
System.err.println("Unprocessable - Business rule violation");
break;
case 429:
System.err.println("Too Many Requests - Rate limited");
break;
case 500:
System.err.println("Internal Server Error - Try again later");
break;
case 503:
System.err.println("Service Unavailable - PayPal may be down");
break;
default:
System.err.println("Unexpected status: " + response.statusCode());
}
} catch (PayperException ex) {
System.err.println("Request failed: " + ex.getMessage());
}
Extracting Error Details
import io.github.eealba.payper.core.client.PayperResponse;
var response = client.orders()
.capture()
.withId(orderId)
.withBody(captureRequest)
.retrieve()
.toResponse();
if (!response.isSuccessful()) {
System.err.println("Failed to capture order");
System.err.println("Status: " + response.statusCode());
// Access structured error details
var errorEntity = response.toErrorEntity();
System.err.println("Error message: " + errorEntity.message());
// You can also get raw response for detailed analysis
String rawResponse = response.toRawString();
System.err.println("Raw response: " + rawResponse);
} else {
var order = response.toEntity();
System.out.println("Order captured: " + order.id());
}
Working with Response Objects
import io.github.eealba.payper.core.client.PayperResponse;
try {
PayperResponse response = client.orders()
.get()
.withId(orderId)
.retrieve()
.toFuture()
.join();
if (response.statusCode() == 200) {
Order order = response.toEntity();
System.out.println("Order: " + order.id());
} else {
System.err.println("Error status: " + response.statusCode());
System.err.println("Body: " + response.toRawString());
}
} catch (Exception ex) {
System.err.println("Request failed: " + ex.getMessage());
}
Retry Strategies
Simple Retry
import io.github.eealba.payper.core.client.PayperResponse;
public Order getOrderWithRetry(String orderId, int maxRetries) {
int attempts = 0;
while (attempts < maxRetries) {
try {
var response = client.orders()
.get()
.withId(orderId)
.retrieve()
.toResponse();
if (response.isSuccessful()) {
return response.toEntity();
}
// Check if error is retryable
if (isRetryable(response.statusCode()) && attempts < maxRetries - 1) {
attempts++;
System.err.println("Attempt " + attempts + " failed, retrying...");
try {
Thread.sleep(1000 * attempts); // Exponential backoff
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted during retry", ie);
}
} else {
throw new RuntimeException("Request failed: " +
response.statusCode() + " - " + response.toErrorEntity().message());
}
} catch (Exception ex) {
attempts++;
if (attempts >= maxRetries) {
throw ex;
}
}
}
throw new RuntimeException("Max retries exceeded");
}
private boolean isRetryable(int statusCode) {
return statusCode == 429 || // Rate limit
statusCode == 500 || // Internal server error
statusCode == 503; // Service unavailable
}
Exponential Backoff
import java.time.Duration;
import io.github.eealba.payper.core.client.PayperResponse;
public class RetryHelper {
public static <T> T executeWithRetry(
Supplier<PayperResponse<T, ?>> operation,
int maxRetries,
Duration initialDelay) {
int attempts = 0;
Duration delay = initialDelay;
while (true) {
try {
var response = operation.get();
if (response.isSuccessful()) {
return response.toEntity();
}
attempts++;
if (!isRetryable(response.statusCode()) || attempts >= maxRetries) {
throw new RuntimeException("Request failed: " +
response.statusCode() + " - " + response.toErrorEntity().message());
}
System.err.println("Attempt " + attempts + " failed (status " +
response.statusCode() + "), retrying in " + delay.toSeconds() + "s...");
try {
Thread.sleep(delay.toMillis());
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted during retry", ie);
}
// Exponential backoff
delay = delay.multipliedBy(2);
} catch (Exception ex) {
throw new RuntimeException("Request failed", ex);
}
}
}
private static boolean isRetryable(int statusCode) {
return statusCode == 429 || statusCode == 500 ||
statusCode == 503;
}
}
// Usage
var response = RetryHelper.executeWithRetry(
() -> client.orders().get().withId("ORDER-1").retrieve().toResponse(),
3,
Duration.ofSeconds(1)
);
Circuit Breaker Pattern
public class CircuitBreaker {
private int failureCount = 0;
private int threshold = 5;
private long resetTimeout = 60000; // 1 minute
private long lastFailureTime = 0;
private State state = State.CLOSED;
enum State { CLOSED, OPEN, HALF_OPEN }
public <T> T execute(Supplier<T> operation) {
if (state == State.OPEN) {
if (System.currentTimeMillis() - lastFailureTime > resetTimeout) {
state = State.HALF_OPEN;
} else {
throw new RuntimeException("Circuit breaker is OPEN");
}
}
try {
T result = operation.get();
if (state == State.HALF_OPEN) {
state = State.CLOSED;
failureCount = 0;
}
return result;
} catch (PayperException ex) {
failureCount++;
lastFailureTime = System.currentTimeMillis();
if (failureCount >= threshold) {
state = State.OPEN;
}
throw ex;
}
}
}
Error Handling in Async
Using exceptionally()
client.orders()
.get()
.withId(orderId)
.retrieve()
.toFuture()
.thenApply(response -> {
if (response.isSuccessful()) {
return response.toEntity();
} else {
throw new RuntimeException("Request failed: " +
response.statusCode() + " - " + response.toErrorEntity().message());
}
})
.exceptionally(ex -> {
System.err.println("Error: " + ex.getMessage());
return null; // Return fallback value
})
.join();
Using handle()
client.orders()
.capture()
.withId(orderId)
.withBody(captureRequest)
.retrieve()
.toFuture()
.handle((response, ex) -> {
if (ex != null) {
System.err.println("Capture failed: " + ex.getMessage());
return createDefaultOrder();
}
if (response.isSuccessful()) {
return response.toEntity();
} else {
System.err.println("Capture failed with status: " + response.statusCode());
System.err.println("Error: " + response.toErrorEntity().message());
return createDefaultOrder();
}
})
.join();
Best Practices
✅ Do
- Always handle errors - Never ignore exceptions
- Check status codes - Different codes require different handling
- Log errors - Record errors for debugging
- Provide user feedback - Inform users about failures
- Retry transient errors - Use exponential backoff
- Validate input - Catch errors early
- Use circuit breakers - Prevent cascading failures
- Monitor error rates - Track and alert on anomalies
❌ Don't
- Catch generic Exception - Be specific with error handling
- Ignore error details - Response body contains useful info
- Retry indefinitely - Set max retry limits
- Expose sensitive info - Don't show API keys or secrets in errors
- Block on retries - Use async retries when possible
Common Errors and Solutions
| Error | Status | Cause | Solution |
|---|---|---|---|
| Authentication failed | 401 | Invalid credentials | Check Client ID/Secret |
| Resource not found | 404 | Invalid ID or deleted resource | Verify resource ID |
| Validation error | 400 | Invalid request data | Check request format |
| Business rule violation | 422 | Cannot process request | Check resource state |
| Rate limit exceeded | 429 | Too many requests | Implement backoff |
| Internal server error | 500 | PayPal server issue | Retry with backoff |
| Service unavailable | 503 | PayPal maintenance | Retry later |
| Connection timeout | -1 | Network issue | Check connectivity |
Logging Errors
import java.util.logging.Logger;
import java.util.logging.Level;
private static final Logger LOGGER = Logger.getLogger(MyClass.class.getName());
var response = client.orders()
.capture()
.withId(orderId)
.withBody(captureRequest)
.retrieve()
.toResponse();
if (response.isSuccessful()) {
var order = response.toEntity();
LOGGER.info("Order captured: " + order.id());
} else {
LOGGER.log(Level.SEVERE, "Failed to capture order: " + orderId);
LOGGER.severe("Status: " + response.statusCode());
LOGGER.severe("Error message: " + response.toErrorEntity().message());
LOGGER.severe("Raw response: " + response.toRawString());
}
Testing Error Handling
Mock Errors
// For unit testing
@Test
public void testOrderNotFound() {
// Mock client to throw PayperException
when(mockClient.orders().get().withId(any())
.retrieve().toEntity())
.thenThrow(new PayperException("Not found"));
// Test error handling
assertThrows(PayperException.class, () -> {
service.getOrder("INVALID-ID");
});
}
Integration Testing
// Test with real PayPal sandbox
@Test
public void testInvalidOrderCapture() {
var client = CheckoutOrdersApiClient.create();
var response = client.orders()
.capture()
.withId("INVALID-ORDER-ID")
.withBody(OrderCaptureRequest.builder().build())
.retrieve()
.toResponse();
// Verify error response
assertFalse(response.isSuccessful());
assertEquals(404, response.statusCode());
// Verify error details are available
var errorEntity = response.toErrorEntity();
assertNotNull(errorEntity.message());
}
Related Resources
- Configuration Guide - Configure timeouts
- Async Operations - Handle async errors
- API Documentation - Understand API behavior
- PayPal Error Reference - Official error codes