Comprehensive Error Handling in Unsplasharp
This document describes the comprehensive error handling system introduced in Unsplasharp, providing structured exceptions with rich context information for better debugging and error recovery.
Overview
The new error handling system provides:
- Specific Exception Types: Different exceptions for different error scenarios
- Rich Error Context: Detailed information about requests, responses, and timing
- Intelligent Retry Logic: Smart retry decisions based on error types
- Backward Compatibility: Existing methods continue to work as before
- Enhanced Debugging: Correlation IDs and structured logging
Exception Hierarchy
Base Exception: UnsplasharpException
All Unsplasharp-specific exceptions inherit from UnsplasharpException
, which provides:
public abstract class UnsplasharpException : Exception
{
public string? RequestUrl { get; } // The URL that caused the error
public string? HttpMethod { get; } // HTTP method used
public ErrorContext? Context { get; } // Rich error context
}
Specific Exception Types
UnsplasharpHttpException
For HTTP-related errors with status codes:
public class UnsplasharpHttpException : UnsplasharpException
{
public HttpStatusCode? StatusCode { get; } // HTTP status code
public string? ResponseContent { get; } // Response body
public bool IsRetryable { get; } // Whether error is retryable
}
UnsplasharpAuthenticationException
For authentication failures (401 Unauthorized):
var client = new UnsplasharpClient("invalid_app_id");
try {
var photo = await client.GetRandomPhotoAsync();
} catch (UnsplasharpAuthenticationException ex) {
Console.WriteLine($"Authentication failed: {ex.Message}");
// Check your application ID
}
UnsplasharpNotFoundException
For resource not found errors (404 Not Found):
try {
var photo = await client.GetPhotoAsync("invalid_photo_id");
} catch (UnsplasharpNotFoundException ex) {
Console.WriteLine($"Photo '{ex.ResourceId}' not found");
Console.WriteLine($"Resource type: {ex.ResourceType}");
}
UnsplasharpRateLimitException
For rate limit exceeded errors (429 Too Many Requests):
try {
var photos = await client.SearchPhotosAsync("nature");
} catch (UnsplasharpRateLimitException ex) {
Console.WriteLine($"Rate limit exceeded: {ex.RateLimitRemaining}/{ex.RateLimit}");
Console.WriteLine($"Reset time: {ex.RateLimitReset}");
// Wait until reset time or implement exponential backoff
if (ex.RateLimitReset.HasValue) {
var waitTime = ex.RateLimitReset.Value - DateTimeOffset.UtcNow;
await Task.Delay(waitTime);
}
}
UnsplasharpNetworkException
For network-related errors:
try {
var photo = await client.GetRandomPhotoAsync();
} catch (UnsplasharpNetworkException ex) {
if (ex.IsRetryable) {
Console.WriteLine("Network error occurred, retrying...");
// Implement retry logic
} else {
Console.WriteLine("Permanent network error");
}
}
UnsplasharpTimeoutException
For request timeout errors:
try {
var photo = await client.GetRandomPhotoAsync();
} catch (UnsplasharpTimeoutException ex) {
Console.WriteLine($"Request timed out after {ex.Timeout}");
// Consider increasing timeout or checking network conditions
}
UnsplasharpParsingException
For JSON parsing errors:
try {
var photo = await client.GetRandomPhotoAsync();
} catch (UnsplasharpParsingException ex) {
Console.WriteLine($"Failed to parse response: {ex.Message}");
Console.WriteLine($"Expected type: {ex.ExpectedType}");
// Log raw content for debugging: ex.RawContent
}
Error Context
Every exception includes an ErrorContext
object with detailed information:
public class ErrorContext
{
public DateTimeOffset Timestamp { get; } // When error occurred
public string? ApplicationId { get; } // Your app ID
public string? CorrelationId { get; } // Unique request ID
public Dictionary<string, string> RequestHeaders { get; }
public Dictionary<string, string> ResponseHeaders { get; }
public RateLimitInfo? RateLimitInfo { get; } // Rate limit details
public Dictionary<string, object> Properties { get; } // Custom properties
}
Using Error Context
try {
var photo = await client.GetRandomPhotoAsync();
} catch (UnsplasharpException ex) {
var context = ex.Context;
Console.WriteLine($"Error occurred at: {context?.Timestamp}");
Console.WriteLine($"Correlation ID: {context?.CorrelationId}");
Console.WriteLine($"Summary: {context?.ToSummary()}");
// Access rate limit information
if (context?.RateLimitInfo != null) {
var rateLimit = context.RateLimitInfo;
Console.WriteLine($"Rate limit: {rateLimit.Remaining}/{rateLimit.Limit}");
}
}
Backward Compatibility
Existing methods continue to work exactly as before, returning null
or empty collections on errors:
// Old approach - still works
var photo = await client.GetRandomPhoto(); // Returns null on error
if (photo == null) {
Console.WriteLine("Failed to get photo");
}
// New approach - throws exceptions
try {
var photo = await client.GetRandomPhotoAsync(); // Throws on error
Console.WriteLine($"Got photo: {photo.Id}");
} catch (UnsplasharpException ex) {
Console.WriteLine($"Error: {ex.Message}");
}
Migration Guide
For New Code
Use the new *Async
methods that throw exceptions:
// Instead of:
var photo = await client.GetRandomPhoto();
if (photo == null) { /* handle error */ }
// Use:
try {
var photo = await client.GetRandomPhotoAsync();
// Use photo
} catch (UnsplasharpException ex) {
// Handle specific error types
}
For Existing Code
No changes required - existing methods maintain their behavior:
// This continues to work unchanged
var photos = await client.SearchPhotos("nature");
if (photos.Count == 0) {
// Handle no results or error
}
Gradual Migration
You can migrate gradually by replacing method calls one at a time:
// Step 1: Replace method call
// var photo = await client.GetRandomPhoto();
var photo = await client.GetRandomPhotoAsync();
// Step 2: Add exception handling
try {
var photo = await client.GetRandomPhotoAsync();
// Use photo
} catch (UnsplasharpNotFoundException) {
// Handle not found
} catch (UnsplasharpRateLimitException ex) {
// Handle rate limiting
await Task.Delay(ex.TimeUntilReset ?? TimeSpan.FromMinutes(1));
} catch (UnsplasharpException ex) {
// Handle other errors
logger.LogError(ex, "Unsplash API error: {Context}", ex.Context?.ToSummary());
}
Best Practices
1. Handle Specific Exception Types
try {
var photo = await client.GetPhotoAsync(photoId);
} catch (UnsplasharpNotFoundException) {
// Photo doesn't exist - show user-friendly message
} catch (UnsplasharpRateLimitException ex) {
// Rate limited - implement backoff
await Task.Delay(ex.TimeUntilReset ?? TimeSpan.FromMinutes(1));
} catch (UnsplasharpNetworkException ex) when (ex.IsRetryable) {
// Transient network error - retry
} catch (UnsplasharpException ex) {
// Other API errors - log and show generic error
logger.LogError(ex, "API error: {Context}", ex.Context?.ToSummary());
}
2. Use Correlation IDs for Debugging
try {
var photo = await client.GetRandomPhotoAsync();
} catch (UnsplasharpException ex) {
logger.LogError(ex, "Request failed [CorrelationId: {CorrelationId}]: {Message}",
ex.Context?.CorrelationId, ex.Message);
}
3. Implement Intelligent Retry Logic
public async Task<Photo> GetPhotoWithRetry(string photoId, int maxRetries = 3) {
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await client.GetPhotoAsync(photoId);
} catch (UnsplasharpNetworkException ex) when (ex.IsRetryable && attempt < maxRetries) {
var delay = TimeSpan.FromSeconds(Math.Pow(2, attempt)); // Exponential backoff
await Task.Delay(delay);
} catch (UnsplasharpRateLimitException ex) when (attempt < maxRetries) {
var delay = ex.TimeUntilReset ?? TimeSpan.FromMinutes(1);
await Task.Delay(delay);
}
}
throw new InvalidOperationException($"Failed to get photo after {maxRetries} attempts");
}
4. Monitor Rate Limits
try {
var photos = await client.SearchPhotosAsync("nature");
} catch (UnsplasharpException ex) {
if (ex.Context?.RateLimitInfo != null) {
var rateLimit = ex.Context.RateLimitInfo;
if (rateLimit.Remaining < 100) {
logger.LogWarning("Rate limit running low: {Remaining}/{Limit}",
rateLimit.Remaining, rateLimit.Limit);
}
}
}
Configuration
The error handling system can be configured through the retry policies:
// The client automatically uses intelligent retry policies
var client = new UnsplasharpClient("your_app_id", logger: logger);
// Retry policies handle:
// - Exponential backoff with jitter
// - Rate limit-aware delays
// - Circuit breaker patterns (optional)
// - Comprehensive logging
Logging Integration
The error handling system integrates with Microsoft.Extensions.Logging:
using var loggerFactory = LoggerFactory.Create(builder =>
builder.AddConsole().SetMinimumLevel(LogLevel.Debug));
var logger = loggerFactory.CreateLogger<UnsplasharpClient>();
var client = new UnsplasharpClient("your_app_id", logger: logger);
// All errors are automatically logged with correlation IDs and context
Summary
The comprehensive error handling system provides:
- ✅ Better Debugging: Specific exception types with rich context
- ✅ Intelligent Retries: Smart retry logic based on error types
- ✅ Rate Limit Awareness: Automatic rate limit detection and handling
- ✅ Backward Compatibility: Existing code continues to work
- ✅ Enhanced Monitoring: Correlation IDs and structured logging
- ✅ Production Ready: Circuit breakers and resilience patterns
This system makes your applications more robust and easier to debug while maintaining full backward compatibility with existing code.