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!
}
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
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;
}
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
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!
}
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;
}
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");
}
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...
}
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...
}
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
}
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);
}
The fix (option 2):
var users = new List<User> { /* users */ };
users.RemoveAll(u => u.IsInactive);
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!
}
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");
}
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
}
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
}
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();
}
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();
}
Exception: async void is only allowed for event handlers:
private async void Button_Click(object sender, EventArgs e)
{
await ProcessDataAsync();
}
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!
}
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();
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)
}
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);
}
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)
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
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!
}
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]+$");
}
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);
}
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);
}
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);
}
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);
}
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;...";
}
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;..."
}
}
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:
-
IDisposableobjects leak resources if not disposed -
async voidis a footgun (exceptions crash the app) -
.Resultcauses deadlocks in UI/web apps -
DateTime.Nowcreates 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)