Top 10 .NET exceptions (part two)

| 8 min. (1547 words)

In Part 1, we walked through the top 5 most common .NET exceptions—breaking down what triggers them and how to fix them. Now, we’re rounding out the list with five more exceptions every .NET developer is bound to encounter at some point:

  1. EntityCommandExecutionException
  2. OutOfMemoryException
  3. KeyNotFoundException
  4. StackOverflowException
  5. InvalidOperationException

These exceptions can stem from database issues, memory mismanagement, and logic errors that can bring your applications to a halt. In this article, we’ll break down each one, explain when and why they occur, and share practical strategies to fix them so you can keep your code running smoothly.

6. EntityCommandExecutionException

EntityCommandExecutionException occurs when an Entity Framework fails to execute a database command. This error often happens during operations like SaveChangesAsync when database constraints (e.g., foreign key violations, unique constraints) are violated, or database connectivity issues arise. Imagine a product catalog system performing updates to product records:

public async Task UpdateProductAsync(ProductDto dto)
{
    var product = await _context.Products.FirstAsync(p => p.Id == dto.Id);
    product.Name = dto.Name;
    product.CategoryId = dto.CategoryId; // Risk of foreign key violation
    await _context.SaveChangesAsync(); // Throws EntityCommandExecutionException
}

This code assumes all CategoryId values are valid. If a CategoryId doesn’t exist in the database, a DbUpdateException (caused by an EntityCommandExecutionException, which also wraps a database specific exception) is thrown. The lack of retry logic also makes it susceptible to transient issues like deadlocks.

Fix

Before saving changes, validate the input data and handle potential exceptions gracefully:

public async Task UpdateProductAsync(ProductDto dto)
{
    var product = await _context.Products.FirstOrDefaultAsync(p => p.Id == dto.Id);
    if (product == null) return;
    
    vat category = await _context.Categories.FirstOrDefaultAsync(c => c.Id == dto.CategoryId);
    if (category == null)
    {
        _logger.LogWarning("Invalid category ID: {CategoryId}", dto.CategoryId);
        return;
    }
    
    product.Name = dto.Name;
    product.CategoryId = dto.CategoryId;
    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateException ex) when (ex.InnerException is SqlException sql && sql.Number == 547)
    {
        _logger.LogError("Database constraint violation: {Message}", ex.Message);
        throw new BusinessException("Invalid data in db update");
    }
}

General tips for handling EntityCommandExecutionExceptions

  • Validate inputs: Always check foreign key and unique constraints before calling SaveChangesAsync.

  • Map errors to business logic: Use SqlException.Number to translate database errors into meaningful application exceptions.

  • Implement retry logic: Use libraries like Polly to handle transient errors like deadlocks.

  • Log errors: Record detailed error information, including SQL error codes and states, for easier debugging.

7. OutOfMemoryException

OutOfMemoryException occurs when an application tries to allocate more memory than is available. This error typically happens when handling large datasets or files without efficient memory management. Here’s a financial application attempting to load a large CSV file into memory:

public List<Transaction> LoadTransactions(string filePath)
{
    var lines = File.ReadAllLines(filePath); // Risk of memory exhaustion
    return lines.Select(ParseTransaction).ToList();
}

This approach is acceptable for small files but fails for large ones. Loading the entire file into memory can lead to a spike in memory usage, eventually causing a crash.

Fix

Process large files line by line to minimize memory usage:

public IEnumerable<Transaction> LoadTransactions(string filePath)
{
    using var reader = new StreamReader(filePath);
    while (!reader.EndOfStream)
    {
        var line = reader.ReadLine();
        if (line != null) yield return ParseTransaction(line);
    }
}

General tips for handling OutOfMemoryExceptions

  • Use streams: Process files incrementally rather than loading them entirely into memory.

  • Implement batch processing: Break large operations into smaller, manageable chunks.

  • Monitor memory: Use tools like the .NET Performance Profiler to detect and fix memory bottlenecks.

  • Dispose of resources: Implement ‘using’ statements or blocks to release memory when resources are no longer needed.

8. KeyNotFoundException

A KeyNotFoundException is thrown when attempting to access a dictionary or JSON property with a key that does not exist. This often occurs when working with JSON data without validating the input. Here’s a JSON parser that assumes all fields are present in the input data:

public User ParseUser(JsonElement userJson)
{
    return new User
    {
        Name = userJson.GetProperty("name").GetString(),
        Email = userJson.GetProperty("email").GetString(),
        Phone = userJson.GetProperty("phone").GetString() // Risk of exception
    };
}

If the phone field is missing, this code will throw a KeyNotFoundException.

Fix

To prevent the exception, check for the existence of the field and provide fallback values:

public User ParseUser(JsonElement userJson)
{
    return new User
    {
        Name = userJson.GetProperty("name").GetString(),
        Email = userJson.GetProperty("email").GetString(),
        Phone = userJson.TryGetProperty("phone", out var phone) ? phone.GetString() : "N/A"
    };
}

General tips for handling KeyNotFoundExceptions

  • Validate inputs: Ensure the structure and completeness of the input data before accessing it.

  • Deserialize the JSON directly: Use the .NET JSON library to deserialize objects into concrete types. This handles missing values automatically:

    var user = JsonSerializer.Deserialize<User>(jsonString);
  • Access properties safely: Use methods like TryGetValue/TryGetProperty or provide default values for missing fields to avoid exceptions.

  • Log issues: Record details of malformed or incomplete data for easier debugging.

9. StackOverflowException

StackOverflowException occurs when a program attempts to use more memory in the call stack than has been allocated to it. This is typically a result of a large number of method calls within a given call tree (through recursion or loops). Still, it can also be caused by declaring excessive local variables within stack frames. This severe exception terminates the process immediately and cannot be caught or handled. Consider a file system scanner that recursively traverses directories to build a tree structure:

public class FileSystemScanner
{
    public DirectoryNode ScanDirectory(string path)
    {
        var node = new DirectoryNode(path);
        node.Children = Directory
            .GetDirectories(path)
            .Select(subDir => ScanDirectory(subDir)) // Recursive call
            .ToList();
        return node;
    }
}

This code is vulnerable in several ways. Recursive calls will exhaust the stack if the directory tree is too deep, and symbolic links or junctions can create infinite loops. Even shallow recursion can consume excessive memory if large or many local variables exist in each stack frame.

Fix

Replace recursive calls with an iterative approach that manually manages the stack:

public DirectoryNode ScanDirectoryIterative(string rootPath)
{
    var root = new DirectoryNode(rootPath);
    var stack = new Stack<(DirectoryNode Node, string Path)>();
    stack.Push((root, rootPath));
    var visitedPaths = new HashSet<string> { rootPath }; // Prevent cycles

    while (stack.Count > 0)
    {
        var (current, path) = stack.Pop();
        foreach (var dir in Directory.GetDirectories(path))
        {
            if (!visitedPaths.Add(dir)) continue; // Skip already visited paths

            var child = new DirectoryNode(dir);
            current.Children.Add(child);
            stack.Push((child, dir));
        }
    }
    return root;
}

General tips for handling StackOverflowExceptions

  • Use iteration: For deep or complex structures, use iterative algorithms over recursion.

  • Track visited nodes: Use a HashSet to handle cycles, such as those caused by symbolic links.

  • Limit depth: Set a maximum depth for traversal to avoid hitting stack limits.

  • Profile memory: Monitor stack usage and memory during development.

10. InvalidOperationException

InvalidOperationException is thrown when an operation for the current state of an object is invalid. A typical scenario is using LINQ’s Single or SingleOrDefault on a sequence that doesn’t meet the uniqueness constraint—either because it has multiple matches or no matches at all. Here’s a permission management service that retrieves user access information. Users may have multiple permissions for a given system, and the database is ordered such that the highest permission for a user and system is first.

public class PermissionService
{
    private readonly List<UserAccess> _userAccessList = new List<UserAccess>
    {
        new UserAccess { Email = "sarah@company.com", System = "CRM", Permission = "Read" },
        new UserAccess { Email = "sarah@company.com", System = "Billing", Permission = "Write" },
        new UserAccess { Email = "sarah@company.com", System = "Billing", Permission = "Read" },
        new UserAccess { Email = "mike@company.com", System = "CRM", Permission = "Admin" }
    };

    public string GetUserPermissionForSystem(string email, string system)
    {
        // Bug: Multiple entries for the same user and system
        var access = _userAccessList.SingleOrDefault(u => 
            u.Email == email && u.System == system);
        return access?.Permission ?? "NoAccess";
    }
}

Because multiple entries exist for a user and system, SingleOrDefault will throw an InvalidOperationException. Uniqueness is not enforced in the database, so using Single or SingleOrDefault is incorrect.

Fix

Use FirstOrDefault as duplicates are acceptable. Since the database maintains an intentional order, FirstOrDeafult is preferable to Any.

public string GetUserPermissionForSystem(string email, string system)
{
    return _userAccessList
        .Where(u => u.Email == email && u.System == system)
        .FirstOrDefault()?.Permission ?? "NoAccess";
}

General tips for handling InvalidOperationExceptions

  • Choose the right LINQ method:

    • Single: Use when enforcing uniqueness is critical. This method will throw an exception in the case of a missing or duplicate value.
    • First: Use when you only need the first match. This method will throw an exception in the case of a missing value.
    • Any: Use to check for existence without fetching data.
    • SingleOrDefault: Use to enforce uniqueness while also providing a default in case of a missing value. This method will still throw an exception if it encounters duplicates.
    • FirstOrDefault: Find the first match and provide a default (null for objects) in case of a missing value.
  • Handle duplicates: Log and manage cases where queries find multiple matches.

  • Ensure data integrity: Prevent duplicates (if uniqueness is a requirement) at the source by enforcing database constraints or performing validation.

Wrap up

With these 10 common .NET exceptions covered, you now have a solid foundation for handling some of the most frequent and frustrating issues in your .NET applications. From null references to stack overflows, better exception management leads to more reliable, maintainable, and performant code—and fewer late-night debugging sessions. Whether it’s catching database errors, managing memory, or avoiding invalid operations, these strategies will help you write cleaner code and ship confidently. Armed with this knowledge, you’re well-equipped to tackle even the trickiest bugs in your .NET projects.

Not a Raygun customer? Try out the full Crash Reporting application free for 14 days!