Top 10 .NET exceptions (part one)
Posted Jan 6, 2025 | 10 min. (2087 words)Exception handling is essential to .NET development, but not all exceptions are equal. Some, like NullReferenceException, surprise developers with unclear stack traces and production crashes. Others, such as MySQLException or HttpRequestException, often point to issues like resource mismanagement or network failures.
At Raygun, we’ve worked with teams around the world to monitor and fix software issues, giving us deep insight into how exceptions occur and how to handle them effectively.
This article covers common exceptions in .NET, the scenarios that trigger them, and practical strategies to handle them. From managing null checks to fixing database connection issues and async cancellations, this guide will help you write safer, more reliable code.
What’s in the article
1. NullReferenceException
The NullReferenceException is one of the most common and notorious exceptions in .NET development. It occurs when code attempts to access a member of an object (or an element of an array) that is null. This exception is particularly troublesome as it often surfaces in production environments, causing unexpected application failures. It is usually challenging to determine which object created the error from the stack trace alone.
Imagine you’re processing an order and need to send an email notification for high-value purchases. In this case, the system assumes every order has a valid customer and tries to access the customer’s email address—without any checks.
public async Task ProcessOrderBad(Order order)
{
if (order == null)
throw new ArgumentNullException(nameof(order));
var customer = await customerService.GetCustomerAsync(order.CustomerId);
if (order.Total > 1000)
{
// Boom! NullReferenceException here if customer wasn't found
await emailService.SendHighValueOrderAlert(customer.Email);
}
await ProcessOrderItems(order);
}
The problem is that we’re not checking if the customer object is null before attempting to access its Email property. If customerService.GetCustomerAsync can’t find a customer for the given CustomerId, it might return null. Trying to access customer.Email in that scenario will throw a NullReferenceException.
Fix
Before accessing the Email property, ensure that the customer object is not null. This check prevents the NullReferenceException by handling the scenario where the customer might not be found.
if (customer == null)
{
throw new InvalidOperationException("Customer not found.");
}
General tips for handling NullReferenceExceptions
- Use the Null Object Pattern: Instead of null, return an empty object that defines default behavior.
public class NullCustomer : ICustomer
{
public string Email => string.Empty;
}
- Take advantage of C# 8.0’s nullable reference types: Enable nullable reference types to let the compiler warn you about potential null references during development.
#nullable enable
Customer? customer = await customerService.GetCustomerAsync(order.CustomerId);
- Use the null-coalescing operator (??): Provide default values when a variable is null to ensure safe defaults.
string displayName = username ?? "Guest";
- Use the null conditional operator (?.): Safely access members only if the object is not null.
string? email = customer?.Email;
-
Use a static code analyzer: Tools like SonarQube or ReSharper can identify potential null reference issues during development.
-
Write unit tests: Specifically test null scenarios to ensure that your application handles them gracefully.
2. MySqlException
A MySQLException is a common exception in .NET applications that use the MySQL. Data library to interact with MySQL databases. This exception can surface for various reasons, from syntax errors in SQL queries to issues with database connectivity. One frequent cause is exhausting the connection pool—this occurs when too many connections to the database are opened without being properly closed, leaving no available connections for new requests.
During a recent .NET migration, we encountered an issue where switching from using blocks to using statements for database connections led to MySqlExceptions. In web requests with long-running operations, connections were held for the entire duration of the request instead of being released immediately after the database operations were completed. Under high traffic, this exhausted the connection pool, causing exceptions across multiple systems.
public async Task ProcessUpload(IFormFile file)
{
// Using statement holds the connection for entire method scope
using var dbConnection = new SqlConnection(_connectionString);
await dbConnection.OpenAsync();
await VerifyUserPermissions(dbConnection);
// Connection still held during this long operation!
await ProcessLargeFile(file);
// Connection not released until method ends
}
In the code above, the using statement scopes the dbConnection for the entire ProcessUpload method. This means the database connection remains open during the long-running ProcessLargeFile operation, preventing it from being returned to the connection pool promptly. Under high traffic, this can quickly exhaust the available connections, leading to MySqlExceptions.
Fix
Ensure database connections are opened, used, and closed as quickly as possible. By limiting the scope of the using statement to only the database operations, connections are released back to the pool immediately after they’re no longer needed.
public async Task ProcessUpload(IFormFile file)
{
// Using block releases connection after auth check
using (var dbConnection = new SqlConnection(_connectionString))
{
await dbConnection.OpenAsync();
await VerifyUserPermissions(dbConnection);
} // Connection released here!
await ProcessLargeFile(file);
}
General tips for handling MySqlExceptions
-
Implement retry mechanisms: Transient failures, such as temporary connection issues, can often be resolved by retrying the operation after a short delay.
-
Use connection pooling: Use built-in connection pooling features (such as those in ADO.NET) to reuse SqlConnection instances with consistent connection strings. This practice avoids unnecessary opening of connections.
-
Implement database timeouts: Set appropriate timeout values for your database operations to prevent long-running queries from holding connections indefinitely.
-
Make queries faster: Aim for query execution times of less than 1 second by optimizing indexes, avoiding unnecessary data retrieval, and writing efficient SQL queries.
3. HttpRequestException
An HttpRequestException pops up during HTTP requests when there are network issues, server errors, or timeouts. It’s a frequent occurrence in distributed systems and microservices that communicate over HTTP.
Many developers handle HTTP responses by checking status codes to determine success or failure states in their applications. A common pattern is to check for specific status codes like 200 for success or 404 for not found, and then handle the response content accordingly. This approach seems thorough at first glance.
public async Task<Customer> GetCustomer(int id)
{
var response = await _httpClient.GetAsync($"/api/customers/{id}");
if (response.StatusCode == HttpStatusCode.OK)
{
return await response.Content.ReadFromJsonAsync<Customer>();
}
else
{
return null;
}
}
However, the above pattern overlooks a crucial aspect of HTTP communication - network-level failures. Even before your code gets a chance to examine the status code, the request could throw an HttpRequestException due to connection timeouts, DNS failures, SSL issues, or other infrastructure problems. Code that only handles status codes will break when these lower-level failures occur.
Fix
The solution is to implement proper exception handling around the HTTP request itself. By wrapping the request in a try-catch block that catches HttpRequestException, you can handle both status code responses and network-level failures. This creates a more resilient application that can gracefully handle application-level HTTP status codes and infrastructure and network issues that inevitably occur in distributed systems.
public async Task<Customer> GetCustomer(int id)
{
try
{
var response = await _httpClient.GetAsync($"/api/customers/{id}");
if (response.StatusCode == HttpStatusCode.OK)
{
return await response.Content.ReadFromJsonAsync<Customer>();
}
else if (response.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
throw new ApplicationException($"Unexpected status code: {response.StatusCode}");
}
catch (HttpRequestException ex)
{
// Handle network/infrastructure failures
_logger.LogError(ex, "Network error while fetching customer {CustomerId}", id);
throw; // Or handle it according to your error strategy
}
}
General tips for handling HttpRequestExceptions
-
Add retry policies and set appropriate timeouts: Implement retry mechanisms for transient failures and configure timeouts to prevent hanging requests.
-
Handle common status codes appropriately: Different HTTP status codes represent different issues. Handle them specifically to provide meaningful responses and actions.
-
Use a crash reporting service: Integrating a crash reporting service like Raygun helps capture and report errors experienced by clients, enabling developers to monitor and address issues proactively.
-
Inspect HttpRequestException details: The HttpRequestException exposes diagnostic information through its StatusCode and InnerException properties. Common inner exceptions include SocketException (for network connectivity issues) and AuthenticationException (for SSL/TLS failures), helping you pinpoint the cause of the HttpRequestException.
4. TaskCanceledException
A TaskCanceledException happens when an async operation gets canceled before it finishes. This can be triggered explicitly using a CancellationToken or implicitly, like when an HTTP request times out.
For example, think about a product search API. In this case, we’re trying to extend the timeout period for long-running searches, but there’s a fundamental misunderstanding of how HttpClient handles timeouts versus cancellation tokens.
In the problematic version, we create a CancellationTokenSource with a 180-second timeout, thinking this will override the default timeout:
public async Task<List<Product>> SearchProductsBad(string query)
{
// We want a longer timeout period
CancellationTokenSource timeoutCts = new(TimeSpan.FromSeconds(180));
try
{
var response = await _httpClient.GetAsync($"/api/products?q={query}", timeoutCts.Token);
return await response.Content.ReadFromJsonAsync<List<Product>>();
}
catch (TaskCancelledException ex)
{
Console.WriteLine($"Exception details: {ex.Message}");
// Exception details: The request was canceled due to the configured HttpClient.Timeout of 100 seconds elapsing.
}
}
The issue here is that creating a CancellationTokenSource with a 180-second timeout won’t override HttpClient’s built-in 100-second timeout. The HttpClient.Timeout property takes precedence, so the request will still timeout after 100 seconds, regardless of the CancellationToken’s timeout value.
Fix
We must properly extend the timeout period by directly modifying the HttpClient’s Timeout property. The request will now wait 180 seconds before timing out, rather than being constrained by the default 100-second timeout regardless of the CancellationToken settings.
public async Task<List<Product>> SearchProducts(string query)
{
_httpClient.Timeout = TimeSpan.FromSeconds(180);
try
{
var response = await _httpClient.GetAsync($"/api/products?q={query}");
return await response.Content.ReadFromJsonAsync<List<Product>>();
}
catch (TaskCancelledException ex)
{
Console.WriteLine($"Exception details: {ex.Message}");
// Our exception handling can remain unchanged.
}
}
General tips for handling TaskCancelledException
- Always pass a CancellationToken through the call chain: Ensure that every asynchronous method accepts and forwards a CancellationToken to allow callers to cancel operations as needed. For example, have a token that allows for user cancellation to avoid maintaining unnecessary query connections.
public async Task<List<Product>> SearchProducts(string query, CancellationToken userCt)
{
var response = await _httpClient.GetAsync($"/api/products?q={query}", userCt);
}
- Clean up resources when cancellation occurs: Ensure that all resources, such as database connections or file handles, are properly disposed of when an operation is canceled. For example, implement using blocks to dispose of resources automatically.
using (var resource = new Resource())
{
// do work
} // Resource disposed here
-
Set appropriate timeouts: Configure timeout durations that align with the expected duration of operations. This simplifies your code as manually requesting cancellation is not required.
-
Implement cooperative cancellation for asynchronous programming: Ensure long running async methods periodically check the cancellation token.
public async Task DoWorkAsync(CancellationToken token)
{
for (int i = 0; i < 100; i++)
{
if (token.IsCancellationRequested)
{
return
}
// Do work
}
}
- Use CancellationToken.None to explicitly disable cancellation: When implementing methods that accept cancellation tokens, use CancellationToken.None to explicitly indicate that cancellation is intentionally disabled rather than accidentally omitted.
await SearchProducts(query, CancellationToken.None);
5. AggregateException
An AggregateException occurs when multiple errors occur during parallel or batch operations. It serves as a container for multiple exceptions that occur during parallel processing, task aggregation, or batch operations, allowing you to handle different types of failures collectively.
Consider a system processing multiple orders in parallel. Each order processing could fail for different reasons (timeout, not found, validation), and we need to handle these failures collectively while maintaining useful error information.
Public void ProcessOrdersBad(List<Order> orders)
{
try
{
Task.WaitAll(orders.Select(order => ProcessOrder(order)).ToArray());
}
catch (AggregateException ae)
{
_logger.LogError("Exception processing orders");
}
}
The code doesn’t handle the possibility of multiple failures occurring during parallel processing. When multiple tasks fail, the exceptions are bundled into an AggregateException, but the code doesn’t extract or handle these individual failures appropriately.
Fix
By flattening the exception, we can remove nested instances of the AggregateException making it easy to determine the root causes.
public void ProcessOrders(List<Order> orders)
{
var tasks = orders.Select(order => ProcessOrder(order));
try
{
Task.WaitAll(tasks.ToArray());
}
catch (AggregateException ae)
{
ae = ae.Flatten();
var timeouts = ae.InnerExceptions.OfType<TimeoutException>().Count();
var notFounds = ae.InnerExceptions.OfType<NotFoundException>().Count();
var validations = ae.InnerExceptions.OfType<ValidationException>().Count();
// Log as appropriate
}
}
General tips for handling AggregateException
- Flatten the exception: Use ae.Flatten() to simplify nested AggregateException instances, making it easier to process all inner exceptions. This method removes any nested AggregateExceptions.
catch (AggregateException ae)
{
ae = ae.Flatten();
// Process ae.InnerExceptions
}
- Group and analyze inner exceptions by type: Categorize exceptions to provide better error reporting and take specific actions based on exception types.
var validationErrors = ae.InnerExceptions.OfType<ValidationException>();
- Use the TaskStatus Enum to identify failed tasks: The TaskStatus enum lets you determine which tasks have failed, enabling targeted error handling and recovery.
var failedTasks = tasks.Where(t => t.Status == TaskStatus.Faulted);
Wrap up
By understanding these exceptions and how to manage them, you can prevent small oversights from turning into major failures. Better exception handling leads to more stable, maintainable applications—and happier managers. Stay tuned for part 2 coming soon, where we cover how to tackle StackOverflow, IndexOutOfRange, OutOfMemory, FileNotFound and InvalidOperation exceptions.