DEV Community

IronSoftware
IronSoftware

Posted on

Common Mistakes That C# Developers Make (And How to Fix Them

I've reviewed thousands of pull requests and interviewed hundreds of C# developers. The same mistakes appear over and over—not because developers are careless, but because C# has subtle gotchas that aren't obvious until you've been burned.

Here are the most common C# mistakes I see, and how to avoid them.

1. Not Disposing IDisposable Objects

The mistake:

public void ProcessFile(string path)
{
    var stream = new FileStream(path, FileMode.Open);
    // Process file...
    // Forgot to dispose stream!
}
Enter fullscreen mode Exit fullscreen mode

Why it's bad:

File handles, database connections, and HTTP clients hold unmanaged resources. If you don't dispose them, they leak memory and file locks until the garbage collector runs (which may be never in long-running apps).

The fix:

public void ProcessFile(string path)
{
    using (var stream = new FileStream(path, FileMode.Open))
    {
        // Process file...
    } // Stream automatically disposed here
}

// Or with C# 8+ syntax:
public void ProcessFile(string path)
{
    using var stream = new FileStream(path, FileMode.Open);
    // Process file...
} // Stream disposed at end of method
Enter fullscreen mode Exit fullscreen mode

Rule: If a class implements IDisposable, always use using or manually call .Dispose().

2. Catching Exceptions Too Broadly

The mistake:

try
{
    var result = await _apiClient.FetchDataAsync();
    return result;
}
catch (Exception ex)
{
    // Swallow all exceptions
    return null;
}
Enter fullscreen mode Exit fullscreen mode

Why it's bad:

You're catching NullReferenceException, OutOfMemoryException, StackOverflowException—exceptions you can't recover from. Worse, you're hiding bugs.

The fix:

try
{
    var result = await _apiClient.FetchDataAsync();
    return result;
}
catch (HttpRequestException ex)
{
    _logger.LogError(ex, "API request failed");
    throw; // Re-throw to let caller handle
}
catch (TaskCanceledException ex)
{
    _logger.LogWarning("API request timed out");
    return null; // Timeout is expected, return null
}
// Don't catch Exception—let unexpected errors bubble up
Enter fullscreen mode Exit fullscreen mode

Rule: Catch specific exceptions you can handle. Let unexpected exceptions crash (you want to know about bugs).

3. Not Using async/await Correctly

The mistake:

public Task<string> FetchDataAsync()
{
    var result = _httpClient.GetStringAsync("https://api.example.com");
    return result; // Forgot await!
}
Enter fullscreen mode Exit fullscreen mode

Why it's bad:

You're returning the Task directly without awaiting, which means exceptions are deferred until the caller awaits. This makes debugging harder.

The fix:

public async Task<string> FetchDataAsync()
{
    var result = await _httpClient.GetStringAsync("https://api.example.com");
    return result;
}
Enter fullscreen mode Exit fullscreen mode

Exception: It's okay to omit await if you're just passing the Task through:

// This is fine (no await needed):
public Task<string> FetchDataAsync()
{
    return _httpClient.GetStringAsync("https://api.example.com");
}
Enter fullscreen mode Exit fullscreen mode

Rule: If you're doing work before or after the async call, use async/await. If you're just passing the Task through, you can omit it.

4. Using .Result or .Wait() (Deadlock Risk)

The mistake:

public void ProcessData()
{
    var data = FetchDataAsync().Result; // DEADLOCK RISK!
    // Process data...
}
Enter fullscreen mode Exit fullscreen mode

Why it's bad:

In ASP.NET and WPF/WinForms, this causes deadlocks. The async method tries to resume on the UI/request thread, but that thread is blocked waiting for .Result.

The fix:

public async Task ProcessDataAsync()
{
    var data = await FetchDataAsync();
    // Process data...
}
Enter fullscreen mode Exit fullscreen mode

Rule: Never use .Result or .Wait() in UI or web applications. Always use async/await all the way up.

5. Modifying Collections While Iterating

The mistake:

var users = new List<User> { /* users */ };

foreach (var user in users)
{
    if (user.IsInactive)
        users.Remove(user); // EXCEPTION: Collection was modified
}
Enter fullscreen mode Exit fullscreen mode

Why it's bad:

Throws InvalidOperationException: Collection was modified.

The fix (option 1):

var users = new List<User> { /* users */ };
var inactiveUsers = users.Where(u => u.IsInactive).ToList();

foreach (var user in inactiveUsers)
{
    users.Remove(user);
}
Enter fullscreen mode Exit fullscreen mode

The fix (option 2):

var users = new List<User> { /* users */ };
users.RemoveAll(u => u.IsInactive);
Enter fullscreen mode Exit fullscreen mode

Rule: Don't modify a collection while iterating. Use ToList() to create a snapshot, or use .RemoveAll().

6. Not Checking for Null

The mistake:

public void ProcessUser(User user)
{
    Console.WriteLine(user.Name.ToUpper()); // NullReferenceException if user or Name is null!
}
Enter fullscreen mode Exit fullscreen mode

The fix (C# 8+ nullable reference types):

#nullable enable
public void ProcessUser(User? user)
{
    if (user == null)
        throw new ArgumentNullException(nameof(user));

    Console.WriteLine(user.Name?.ToUpper() ?? "Unknown");
}
Enter fullscreen mode Exit fullscreen mode

Rule: Enable nullable reference types (#nullable enable) and handle nulls explicitly.

7. Using == Instead of .Equals() for Strings

The mistake:

string input = GetUserInput();
if (input == "admin") // This works, but...
{
    // Grant admin access
}
Enter fullscreen mode Exit fullscreen mode

Why it's subtle:

== works for string literals but can fail with string interning edge cases. For case-insensitive comparisons, it's wrong.

The fix:

string input = GetUserInput();
if (string.Equals(input, "admin", StringComparison.OrdinalIgnoreCase))
{
    // Grant admin access
}
Enter fullscreen mode Exit fullscreen mode

Rule: Use string.Equals() with StringComparison for explicit, predictable behavior.

8. Forgetting to Await Async Void Methods

The mistake:

public async void ProcessDataAsync() // async void is dangerous!
{
    await _apiClient.FetchDataAsync();
}
Enter fullscreen mode Exit fullscreen mode

Why it's bad:

You can't await async void methods, and exceptions crash the app instead of being catchable.

The fix:

public async Task ProcessDataAsync()
{
    await _apiClient.FetchDataAsync();
}
Enter fullscreen mode Exit fullscreen mode

Exception: async void is only allowed for event handlers:

private async void Button_Click(object sender, EventArgs e)
{
    await ProcessDataAsync();
}
Enter fullscreen mode Exit fullscreen mode

Rule: Use async Task, not async void (except for event handlers).

9. Not Using StringBuilder for String Concatenation in Loops

The mistake:

string result = "";
for (int i = 0; i < 1000; i++)
{
    result += $"Line {i}\n"; // Creates 1000 string objects!
}
Enter fullscreen mode Exit fullscreen mode

Why it's bad:

Strings are immutable. Each concatenation creates a new string object, causing O(n²) performance.

The fix:

var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
    sb.AppendLine($"Line {i}");
}
string result = sb.ToString();
Enter fullscreen mode Exit fullscreen mode

Rule: Use StringBuilder for concatenation in loops.

10. Ignoring ConfigureAwait(false) in Libraries

The mistake:

public async Task<string> FetchDataAsync()
{
    return await _httpClient.GetStringAsync("https://api.example.com");
    // Resumes on captured context (UI thread in WPF, request context in ASP.NET)
}
Enter fullscreen mode Exit fullscreen mode

Why it's bad:

In libraries, you don't need to resume on the original context. This adds overhead.

The fix:

public async Task<string> FetchDataAsync()
{
    return await _httpClient.GetStringAsync("https://api.example.com")
        .ConfigureAwait(false);
}
Enter fullscreen mode Exit fullscreen mode

Rule: Use .ConfigureAwait(false) in library code. Don't use it in UI code (you need the UI thread).

11. Using DateTime.Now Instead of DateTime.UtcNow

The mistake:

var timestamp = DateTime.Now; // Local time (timezone-dependent)
Enter fullscreen mode Exit fullscreen mode

Why it's bad:

DateTime.Now is in the server's local timezone. If your server is in New York and your user is in Tokyo, timestamps are ambiguous.

The fix:

var timestamp = DateTime.UtcNow; // Always UTC
Enter fullscreen mode Exit fullscreen mode

Rule: Use DateTime.UtcNow for timestamps. Convert to local time only when displaying to users.

12. Not Validating User Input

The mistake:

public void CreateUser(string email)
{
    _database.Insert(new User { Email = email }); // No validation!
}
Enter fullscreen mode Exit fullscreen mode

Why it's bad:

Users can submit malformed emails, SQL injection attempts, or malicious data.

The fix:

public void CreateUser(string email)
{
    if (string.IsNullOrWhiteSpace(email))
        throw new ArgumentException("Email is required", nameof(email));

    if (!IsValidEmail(email))
        throw new ArgumentException("Invalid email format", nameof(email));

    _database.Insert(new User { Email = email });
}

private bool IsValidEmail(string email)
{
    return System.Text.RegularExpressions.Regex.IsMatch(email,
        @"^[^@\s]+@[^@\s]+\.[^@\s]+$");
}
Enter fullscreen mode Exit fullscreen mode

Rule: Always validate user input at system boundaries (API controllers, form handlers).

13. Using Task.Run() in ASP.NET

The mistake:

[HttpGet]
public async Task<IActionResult> GetData()
{
    var result = await Task.Run(() =>
    {
        return _database.FetchData(); // Offloading to thread pool
    });

    return Ok(result);
}
Enter fullscreen mode Exit fullscreen mode

Why it's bad:

ASP.NET is already async. Using Task.Run() steals a thread pool thread, reducing scalability.

The fix:

[HttpGet]
public async Task<IActionResult> GetData()
{
    var result = await _database.FetchDataAsync(); // Truly async
    return Ok(result);
}
Enter fullscreen mode Exit fullscreen mode

Rule: Don't use Task.Run() in ASP.NET unless you're doing CPU-bound work.

14. Not Using CancellationToken for Long-Running Operations

The mistake:

public async Task<string> ProcessLargeFile(string path)
{
    using var stream = File.OpenRead(path);
    // Long-running operation with no cancellation support
    return await ReadAllAsync(stream);
}
Enter fullscreen mode Exit fullscreen mode

Why it's bad:

If the user cancels the request (closes browser), the operation continues wasting resources.

The fix:

public async Task<string> ProcessLargeFile(string path, CancellationToken cancellationToken)
{
    using var stream = File.OpenRead(path);
    return await ReadAllAsync(stream, cancellationToken);
}

// In ASP.NET Controller:
[HttpGet]
public async Task<IActionResult> ProcessFile(CancellationToken cancellationToken)
{
    var result = await ProcessLargeFile("large-file.txt", cancellationToken);
    return Ok(result);
}
Enter fullscreen mode Exit fullscreen mode

Rule: Accept CancellationToken in async methods, especially in ASP.NET.

15. Hardcoding Connection Strings

The mistake:

public class UserRepository
{
    private const string ConnectionString = "Server=localhost;Database=MyDb;...";
}
Enter fullscreen mode Exit fullscreen mode

Why it's bad:

Connection strings differ between dev, staging, and production. Hardcoding forces code changes for deployments.

The fix:

public class UserRepository
{
    private readonly string _connectionString;

    public UserRepository(IConfiguration configuration)
    {
        _connectionString = configuration.GetConnectionString("MyDb");
    }
}

// In appsettings.json:
{
  "ConnectionStrings": {
    "MyDb": "Server=localhost;Database=MyDb;..."
  }
}
Enter fullscreen mode Exit fullscreen mode

Rule: Store configuration (connection strings, API keys) in appsettings.json or environment variables.

Quick Reference: Mistakes to Avoid

Mistake Fix
Not disposing IDisposable Use using statements
Catching Exception broadly Catch specific exceptions
Missing await in async methods Always await async calls
Using .Result or .Wait() Use async/await
Modifying collections while iterating Use .ToList() or .RemoveAll()
Not checking for null Enable nullable reference types
Using == for strings Use string.Equals() with StringComparison
async void methods Use async Task
String concatenation in loops Use StringBuilder
Ignoring .ConfigureAwait(false) in libraries Add .ConfigureAwait(false)
Using DateTime.Now Use DateTime.UtcNow
Not validating user input Validate at system boundaries
Using Task.Run() in ASP.NET Use truly async APIs
Not using CancellationToken Accept and pass CancellationToken
Hardcoding configuration Use IConfiguration

Conclusion

Most C# mistakes aren't about not knowing the language—they're about not knowing the gotchas:

  • IDisposable objects leak resources if not disposed
  • async void is a footgun (exceptions crash the app)
  • .Result causes deadlocks in UI/web apps
  • DateTime.Now creates timezone bugs

Learn these patterns once, and you'll write cleaner, more reliable C# code.


Written by Jacob Mellor, CTO at Iron Software. Jacob created IronPDF and leads a team of 50+ engineers building .NET document processing libraries.

Top comments (0)