Exception handling is crucial when working with HttpClient in C# because network operations can fail for various reasons. This guide covers the common exceptions and proper handling strategies.
Common HttpClient Exceptions
1. HttpRequestException
Thrown for network-related errors, DNS failures, and connection issues:
using System;
using System.Net.Http;
using System.Threading.Tasks;
public async Task HandleHttpRequestException()
{
    using var client = new HttpClient();
    try
    {
        var response = await client.GetAsync("https://nonexistent-domain.com");
        var content = await response.Content.ReadAsStringAsync();
    }
    catch (HttpRequestException ex)
    {
        Console.WriteLine($"Network error: {ex.Message}");
        // Check inner exception for more details
        if (ex.InnerException != null)
        {
            Console.WriteLine($"Inner exception: {ex.InnerException.Message}");
        }
    }
}
2. TaskCanceledException
Thrown when requests are canceled or timeout:
public async Task HandleTimeouts()
{
    using var client = new HttpClient();
    client.Timeout = TimeSpan.FromSeconds(5); // 5-second timeout
    try
    {
        var response = await client.GetAsync("https://httpbin.org/delay/10");
    }
    catch (TaskCanceledException ex)
    {
        if (ex.CancellationToken.IsCancellationRequested)
        {
            Console.WriteLine("Request was explicitly canceled");
        }
        else
        {
            Console.WriteLine("Request timed out");
        }
    }
}
3. ArgumentException
Thrown for invalid URIs or parameters:
public async Task HandleInvalidUri()
{
    using var client = new HttpClient();
    try
    {
        var response = await client.GetAsync("invalid-uri");
    }
    catch (ArgumentException ex)
    {
        Console.WriteLine($"Invalid URI: {ex.Message}");
    }
    catch (UriFormatException ex)
    {
        Console.WriteLine($"Malformed URI: {ex.Message}");
    }
}
Comprehensive Exception Handling Pattern
Here's a robust exception handling pattern that covers all common scenarios:
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
public class HttpClientService
{
    private readonly HttpClient _httpClient;
    public HttpClientService()
    {
        _httpClient = new HttpClient()
        {
            Timeout = TimeSpan.FromSeconds(30)
        };
    }
    public async Task<string> GetDataAsync(string url, CancellationToken cancellationToken = default)
    {
        try
        {
            using var response = await _httpClient.GetAsync(url, cancellationToken);
            // Handle HTTP status codes
            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            // Handle specific HTTP status codes
            switch (response.StatusCode)
            {
                case HttpStatusCode.NotFound:
                    throw new InvalidOperationException("Resource not found");
                case HttpStatusCode.Unauthorized:
                    throw new UnauthorizedAccessException("Authentication required");
                case HttpStatusCode.Forbidden:
                    throw new UnauthorizedAccessException("Access forbidden");
                case HttpStatusCode.InternalServerError:
                    throw new InvalidOperationException("Server error occurred");
                default:
                    throw new HttpRequestException($"HTTP error: {response.StatusCode} - {response.ReasonPhrase}");
            }
        }
        catch (HttpRequestException ex)
        {
            // Network connectivity issues, DNS failures, server unreachable
            throw new InvalidOperationException($"Network error occurred: {ex.Message}", ex);
        }
        catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
        {
            // Request timeout
            throw new TimeoutException("Request timed out", ex);
        }
        catch (TaskCanceledException ex) when (cancellationToken.IsCancellationRequested)
        {
            // Explicit cancellation
            throw new OperationCanceledException("Request was canceled", ex, cancellationToken);
        }
        catch (ArgumentException ex)
        {
            // Invalid URI or parameters
            throw new ArgumentException($"Invalid request parameters: {ex.Message}", ex);
        }
    }
    public void Dispose()
    {
        _httpClient?.Dispose();
    }
}
Using EnsureSuccessStatusCode()
For simpler scenarios, you can use EnsureSuccessStatusCode() to automatically throw exceptions for non-success status codes:
public async Task<string> GetDataWithEnsureSuccess(string url)
{
    using var client = new HttpClient();
    try
    {
        var response = await client.GetAsync(url);
        response.EnsureSuccessStatusCode(); // Throws HttpRequestException for non-success codes
        return await response.Content.ReadAsStringAsync();
    }
    catch (HttpRequestException ex)
    {
        // This will catch both network errors and HTTP error status codes
        Console.WriteLine($"Request failed: {ex.Message}");
        throw;
    }
}
Retry Logic with Exception Handling
Implement retry logic for transient failures:
public async Task<string> GetDataWithRetry(string url, int maxRetries = 3)
{
    using var client = new HttpClient();
    var attempt = 0;
    while (attempt < maxRetries)
    {
        try
        {
            var response = await client.GetAsync(url);
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
        catch (HttpRequestException ex) when (attempt < maxRetries - 1)
        {
            attempt++;
            Console.WriteLine($"Attempt {attempt} failed: {ex.Message}. Retrying...");
            await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt))); // Exponential backoff
        }
        catch (TaskCanceledException ex) when (!ex.CancellationToken.IsCancellationRequested && attempt < maxRetries - 1)
        {
            // Retry on timeout
            attempt++;
            Console.WriteLine($"Timeout on attempt {attempt}. Retrying...");
            await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
        }
    }
    throw new InvalidOperationException($"Failed to get data after {maxRetries} attempts");
}
Best Practices
- Always use async/await for HttpClient operations to avoid blocking threads
- Reuse HttpClient instances rather than creating new ones for each request
- Set appropriate timeouts to prevent hanging requests
- Handle specific exceptions rather than using generic catch-all blocks
- Implement retry logic for transient failures
- Log exceptions with sufficient detail for debugging
- Use cancellation tokens for long-running operations
- Dispose HttpClient properly or use dependency injection with IHttpClientFactory
Remember that proper exception handling makes your applications more robust and provides better user experience when network issues occur.