Skip to content

Error Handling

dblike edited this page Jan 20, 2026 · 1 revision

Error Handling Guide

LichessSharp provides a comprehensive exception hierarchy for handling API errors. This guide covers exception types, retry strategies, and best practices.


Exception Hierarchy

All LichessSharp exceptions inherit from LichessException:

LichessException (base class)
├── LichessRateLimitException    (HTTP 429)
├── LichessAuthenticationException (HTTP 401)
├── LichessAuthorizationException  (HTTP 403)
├── LichessNotFoundException       (HTTP 404)
└── LichessValidationException     (HTTP 400)

Exception Types

LichessException

The base exception for all Lichess API errors.

Property Type Description
Message string Error description
StatusCode HttpStatusCode? HTTP status code if available
LichessError string? Error message from Lichess API
InnerException Exception? Underlying exception if any

Example:

try
{
    await client.Users.GetAsync("nonexistent-user");
}
catch (LichessException ex)
{
    Console.WriteLine($"Error: {ex.Message}");
    Console.WriteLine($"Status: {ex.StatusCode}");
    Console.WriteLine($"Lichess says: {ex.LichessError}");
}

LichessRateLimitException

Thrown when the API rate limit is exceeded (HTTP 429).

Property Type Description
RetryAfter TimeSpan? Recommended wait time before retrying

Example:

try
{
    // Making many rapid requests
    foreach (var username in usernames)
    {
        await client.Users.GetAsync(username);
    }
}
catch (LichessRateLimitException ex)
{
    Console.WriteLine($"Rate limited! Wait {ex.RetryAfter?.TotalSeconds ?? 60} seconds");

    if (ex.RetryAfter.HasValue)
    {
        await Task.Delay(ex.RetryAfter.Value);
        // Retry the request
    }
}

LichessAuthenticationException

Thrown when authentication fails (HTTP 401). Usually means invalid or expired token.

Example:

try
{
    using var client = new LichessClient("invalid_token");
    await client.Account.GetProfileAsync();
}
catch (LichessAuthenticationException ex)
{
    Console.WriteLine("Invalid token. Please generate a new one at:");
    Console.WriteLine("https://lichess.org/account/oauth/token");
}

LichessAuthorizationException

Thrown when access is forbidden (HTTP 403). Usually means missing OAuth scopes.

Property Type Description
RequiredScope string? The OAuth scope needed for this operation

Example:

try
{
    // Trying to send a message without msg:write scope
    await client.Messaging.SendAsync("username", "Hello!");
}
catch (LichessAuthorizationException ex)
{
    Console.WriteLine($"Missing permission. Required scope: {ex.RequiredScope}");
    Console.WriteLine("Generate a new token with the required scope.");
}

LichessNotFoundException

Thrown when a requested resource doesn't exist (HTTP 404).

Example:

try
{
    var user = await client.Users.GetAsync("this-user-does-not-exist");
}
catch (LichessNotFoundException)
{
    Console.WriteLine("User not found");
}

LichessValidationException

Thrown when request validation fails (HTTP 400).

Property Type Description
ValidationErrors IReadOnlyDictionary<string, string[]>? Field-specific validation errors

Example:

try
{
    await client.Challenges.CreateAsync("opponent", new ChallengeCreateOptions
    {
        ClockLimit = -1  // Invalid
    });
}
catch (LichessValidationException ex)
{
    Console.WriteLine($"Validation failed: {ex.Message}");

    if (ex.ValidationErrors != null)
    {
        foreach (var (field, errors) in ex.ValidationErrors)
        {
            Console.WriteLine($"  {field}: {string.Join(", ", errors)}");
        }
    }
}

Automatic Rate Limit Handling

LichessSharp can automatically handle rate limits by waiting and retrying.

Default Behavior

By default, the client will:

  1. Catch HTTP 429 responses
  2. Read the Retry-After header
  3. Wait the specified duration
  4. Retry up to 3 times
// Default: auto-retry enabled with 3 attempts
using var client = new LichessClient(token);

Custom Configuration

using var client = new LichessClient(token, new LichessClientOptions
{
    // Disable automatic retries
    AutoRetryOnRateLimit = false,

    // Or configure retry behavior
    AutoRetryOnRateLimit = true,
    MaxRateLimitRetries = 5,

    // For long-running bots, retry indefinitely
    UnlimitedRateLimitRetries = true
});

Manual Rate Limit Handling

If you disable auto-retry, handle rate limits manually:

var options = new LichessClientOptions { AutoRetryOnRateLimit = false };
using var client = new LichessClient(token, options);

async Task<User> GetUserWithRetry(string username, int maxRetries = 3)
{
    for (int attempt = 1; attempt <= maxRetries; attempt++)
    {
        try
        {
            return await client.Users.GetAsync(username);
        }
        catch (LichessRateLimitException ex)
        {
            if (attempt == maxRetries) throw;

            var delay = ex.RetryAfter ?? TimeSpan.FromSeconds(60);
            Console.WriteLine($"Rate limited. Waiting {delay.TotalSeconds}s (attempt {attempt}/{maxRetries})");
            await Task.Delay(delay);
        }
    }
    throw new InvalidOperationException("Unreachable");
}

Transient Failure Handling

LichessSharp automatically retries on transient network failures (DNS errors, connection timeouts).

Default Behavior

  • Enabled by default
  • Retries up to 3 times
  • Exponential backoff with jitter (1s, 2s, 4s...)
  • Maximum delay capped at 30 seconds

Configuration

using var client = new LichessClient(token, new LichessClientOptions
{
    // Disable transient retry
    EnableTransientRetry = false,

    // Or customize
    EnableTransientRetry = true,
    MaxTransientRetries = 5,
    TransientRetryBaseDelay = TimeSpan.FromSeconds(2),
    TransientRetryMaxDelay = TimeSpan.FromMinutes(1)
});

Streaming Error Handling

Streaming endpoints require special handling for long-running connections.

Reconnection Pattern

async Task StreamWithReconnect(CancellationToken ct)
{
    while (!ct.IsCancellationRequested)
    {
        try
        {
            await foreach (var evt in client.Bot.StreamEventsAsync(ct))
            {
                ProcessEvent(evt);
            }
        }
        catch (HttpRequestException ex) when (!ct.IsCancellationRequested)
        {
            Console.WriteLine($"Connection lost: {ex.Message}");
            Console.WriteLine("Reconnecting in 5 seconds...");
            await Task.Delay(TimeSpan.FromSeconds(5), ct);
        }
        catch (OperationCanceledException)
        {
            break;
        }
    }
}

Graceful Shutdown

Always use CancellationToken for clean shutdown:

using var cts = new CancellationTokenSource();

Console.CancelKeyPress += (_, e) =>
{
    e.Cancel = true;
    Console.WriteLine("Shutting down...");
    cts.Cancel();
};

try
{
    await foreach (var evt in client.Bot.StreamEventsAsync(cts.Token))
    {
        // Process events
    }
}
catch (OperationCanceledException)
{
    Console.WriteLine("Stream stopped gracefully");
}

Complete Error Handling Example

using LichessSharp;
using LichessSharp.Exceptions;

async Task<bool> SafeCreateChallenge(ILichessClient client, string opponent)
{
    try
    {
        var challenge = await client.Challenges.CreateAsync(opponent, new ChallengeCreateOptions
        {
            Rated = true,
            ClockLimit = 300,
            ClockIncrement = 3
        });

        Console.WriteLine($"Challenge created: {challenge.Id}");
        return true;
    }
    catch (LichessNotFoundException)
    {
        Console.WriteLine($"User '{opponent}' not found");
        return false;
    }
    catch (LichessAuthorizationException ex)
    {
        Console.WriteLine($"Permission denied. Need scope: {ex.RequiredScope ?? "challenge:write"}");
        return false;
    }
    catch (LichessValidationException ex)
    {
        Console.WriteLine($"Invalid challenge options: {ex.Message}");
        return false;
    }
    catch (LichessRateLimitException ex)
    {
        Console.WriteLine($"Too many requests. Try again in {ex.RetryAfter?.TotalSeconds ?? 60}s");
        return false;
    }
    catch (LichessException ex)
    {
        Console.WriteLine($"Lichess error ({ex.StatusCode}): {ex.LichessError ?? ex.Message}");
        return false;
    }
    catch (HttpRequestException ex)
    {
        Console.WriteLine($"Network error: {ex.Message}");
        return false;
    }
}

Best Practices

1. Catch Specific Exceptions First

try
{
    // API call
}
catch (LichessNotFoundException)
{
    // Handle 404 specifically
}
catch (LichessRateLimitException)
{
    // Handle rate limiting
}
catch (LichessException)
{
    // Handle other Lichess errors
}
catch (Exception)
{
    // Handle unexpected errors
}

2. Use Cancellation Tokens

Always pass cancellation tokens to allow graceful shutdown:

using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
await client.Games.ExportAsync(gameId, cancellationToken: cts.Token);

3. Configure Timeouts Appropriately

var options = new LichessClientOptions
{
    DefaultTimeout = TimeSpan.FromSeconds(60),    // Regular requests
    StreamingTimeout = Timeout.InfiniteTimeSpan   // Streaming (default)
};

4. Log Errors for Debugging

catch (LichessException ex)
{
    logger.LogError(ex, "Lichess API error: Status={Status}, Error={Error}",
        ex.StatusCode, ex.LichessError);
    throw;
}

5. Handle Null Responses

Some methods return null instead of throwing:

var eval = await client.Analysis.GetCloudEvaluationAsync(fen);
if (eval == null)
{
    Console.WriteLine("Position not found in cloud database");
}

See Also

Clone this wiki locally