Top 15 Mistakes .NET Developers Make and How to Avoid Common Pitfalls
Learn the 15 most common mistakes .NET developers make with async, EF Core, HttpClient, and memory, plus simple fixes you can use today.
A quick story before we start
Imagine you are learning to cook. You can follow a recipe and the food still tastes fine. But a few small habits, like leaving the gas on high, or chopping onions with a blunt knife, slowly cause trouble. The food burns. Your fingers hurt. The same thing happens in code.
Most .NET developers do not write broken code. They write code that runs today and slowly causes pain later. It gets slow. It crashes at midnight. It leaks memory. The bug is rarely a big mystery. It is usually one of the same small mistakes, repeated.
This guide walks through 15 of those mistakes. Each one has a plain explanation, a tiny example, and a simple fix. You do not need to be an expert. If you can read a recipe, you can fix these.
How a small mistake becomes a big problem
Steps
Habit
A small bad pattern
Works today
Tests pass locally
Grows under load
Real traffic arrives
Breaks
Slow, crash, or leak
Late fix
Stressful debugging
Mistake 1: Blocking on async code with .Result or .Wait()
This is the most famous .NET trap. You have an async method, but you call it like this.
// Bad: blocks the thread and can deadlock
public string GetName()
{
return GetNameAsync().Result; // freezes in UI / classic ASP.NET
}The problem is that .Result and .Wait() stop the current thread and wait. In some apps, the work inside needs that very same thread to finish. Now both sides wait forever. That is a deadlock.
The fix is simple: use await and let async flow all the way up.
// Good: async all the way
public async Task<string> GetNameAsync()
{
return await FetchNameAsync();
}Mistake 2: Using async void
async void looks normal, but it hides a danger. If the method throws, you cannot catch the error. It can crash your whole app.
// Bad: exception here can crash the process
public async void SaveData() { await DoWorkAsync(); }
// Good: returns a Task you can await and wrap in try/catch
public async Task SaveDataAsync() { await DoWorkAsync(); }The only fair use of async void is an event handler, like a button click. Everywhere else, return Task.
Mistake 3: Forgetting ConfigureAwait in library code
When you write a shared library (a NuGet package, a helper used by many apps), use ConfigureAwait(false). It tells .NET, "I do not need to come back to the original context." This avoids deadlocks and is a little faster.
// In a library
var data = await client.GetStringAsync(url).ConfigureAwait(false);But note the rules. In ASP.NET Core request code you do not need it. In UI apps, skip it when the code after await touches the screen, because you must return to the UI thread.
| Where you are | Use ConfigureAwait(false)? |
|---|---|
| Shared library code | Yes |
| ASP.NET Core controllers | Not needed |
| UI code that updates the screen | No |
| Background worker (no UI) | Yes is fine |
Mistake 4: Creating a new HttpClient every time
HttpClient looks like something you should create and dispose, like a file. It is not. Each new one can keep a network socket open even after you dispose it. Under load, you run out of sockets. This is called socket exhaustion.
// Bad: a new client per request
using var client = new HttpClient();
var res = await client.GetAsync(url);The fix is IHttpClientFactory. Register it once, then ask for clients. It reuses connections safely.
// Program.cs
builder.Services.AddHttpClient();
// In your class
public class WeatherService(IHttpClientFactory factory)
{
public async Task<string> GetAsync()
{
var client = factory.CreateClient();
return await client.GetStringAsync("https://example.com");
}
}HttpClient: the safe path
Steps
Register
AddHttpClient in Program.cs
Inject
IHttpClientFactory
Create
factory.CreateClient()
Reuse
No socket leak
Mistake 5: The N+1 query problem in EF Core
This one quietly kills performance. You load a list, then loop and touch a related thing. EF Core runs one query for the list, then one more query for every row. Ten orders become eleven trips to the database. A thousand orders become a thousand and one.
// Bad: one query for orders, then one per order for Customer
var orders = await db.Orders.ToListAsync();
foreach (var o in orders)
Console.WriteLine(o.Customer.Name); // hidden query each timeFix it with Include, so the related data comes in one trip.
// Good: a single query with a join
var orders = await db.Orders
.Include(o => o.Customer)
.ToListAsync();Mistake 6: Tracking entities you only read
By default EF Core tracks every entity it loads, so it can detect changes for saving. But if you are only showing data on a page, tracking wastes memory and time. Use AsNoTracking for read-only queries.
// Good for read-only screens
var products = await db.Products
.AsNoTracking()
.ToListAsync();| Query type | Tracking choice |
|---|---|
| You will edit and save | Default (tracked) |
| Read-only list or report | AsNoTracking() |
| Big export | AsNoTracking() + streaming |
Mistake 7: Building strings with + in a loop
A string in .NET cannot change. Every + makes a brand new string and copies the old one. In a loop with thousands of items, that copying piles up and slows everything down.
// Bad: creates a new string every loop
string result = "";
foreach (var item in items)
result += item + ", ";
// Good: StringBuilder reuses one buffer
var sb = new StringBuilder();
foreach (var item in items)
sb.Append(item).Append(", ");
string result = sb.ToString();Rule of thumb: a few joins are fine. A loop means reach for StringBuilder.
Mistake 8: Not disposing IDisposable things
Some objects hold real resources: files, database connections, network streams. If you do not dispose them, those resources stay locked until the garbage collector gets around to it, which may be much later. Use using so cleanup is automatic.
// Good: 'using' disposes the stream even if an error happens
using var stream = File.OpenRead("data.txt");
var first = stream.ReadByte();
// stream is closed here, automaticallyThis single keyword prevents a huge class of "file is locked" and "too many open connections" bugs.
Mistake 9: Catching exceptions and hiding them
A scary pattern is the empty catch. The program keeps running, but the real problem is now invisible.
// Bad: the error vanishes with no trace
try { DoWork(); }
catch { }If you catch, do something useful: log it, retry, or rethrow. And never use catch (Exception) to hide bugs you should fix.
// Good: catch what you can handle, and log the rest
try
{
DoWork();
}
catch (IOException ex)
{
logger.LogError(ex, "File step failed");
throw; // let the caller decide
}Mistake 10: Ignoring nullable reference warnings
Modern C# warns you when a value might be null. Many people switch this off or "shut it up" with the ! operator. That just turns a friendly compile-time warning into a real crash later: the dreaded NullReferenceException.
// Bad: silencing the warning, crash waits for you
var name = user!.Name;
// Good: check first, or use safe access
var name = user?.Name ?? "Guest";Keep nullable warnings on. They are like a seatbelt that costs nothing.
Handling a possibly-null value
Steps
May be null
Compiler warns you
Check
if or ?. operator
Default
?? fallback value
Safe
No NRE at runtime
Mistake 11: Putting secrets in appsettings.json
Connection strings, API keys, and passwords do not belong in a file you push to Git. Anyone who sees the repo sees your keys. Use user secrets for local work and a vault or environment variables in production.
// Read config the safe way; the value lives outside source control
var key = builder.Configuration["Payment:ApiKey"];Mistake 12: Doing slow work inside the request
When a web request comes in, the user is waiting. If you send an email, resize an image, or call three slow services inside that request, the user stares at a spinner. Move slow work to a background job or a queue, and reply fast.
Mistake 13: Reinventing things the framework already gives you
New developers often write their own caching, their own logging, their own JSON parser. The built-in tools are tested by millions of people. Use IMemoryCache, ILogger, and System.Text.Json first. Only build your own when you have a clear, measured reason.
One more note for 2026: some popular libraries changed their licensing. MediatR and MassTransit are now commercially licensed for many uses. Check the license before you add them, especially at work. Often a small bit of plain C# does the job without any extra dependency.
Mistake 14: No cancellation support
A long task should be stoppable. If a user closes the page, or a request times out, your code should be able to stop. Pass a CancellationToken through your async calls.
// Good: the token lets callers cancel cleanly
public async Task<Report> BuildAsync(CancellationToken token)
{
var rows = await db.Sales.ToListAsync(token);
token.ThrowIfCancellationRequested();
return Summarize(rows);
}Without this, cancelled work keeps running and wastes your server.
Mistake 15: Not measuring before optimizing
The last mistake is guessing. People rewrite code they think is slow, and the real slow part was somewhere else. Measure first. Use a profiler, logging, or a tool like BenchmarkDotNet. Then fix the part that actually matters.
| Step | What to do |
|---|---|
| 1. Measure | Find the real slow spot |
| 2. Fix one thing | Change the biggest cost |
| 3. Measure again | Confirm it helped |
| 4. Stop | Do not over-tune |
How these mistakes connect
Most of these are not separate problems. They share one root: doing something that feels simple now but costs you later under real load. Async blocking, N+1 queries, new HttpClient each time, and string concatenation in a loop all look harmless in a tiny test. They only bite when traffic grows. The cure is the same everywhere: think about what happens when this runs a thousand times, not once.
If you remember one idea from this whole guide, let it be this: write code for the busy day, not the quiet test. A busy day has many requests, slow networks, big lists, and users who cancel. Code that survives that day is good code.
Start small. Pick the two mistakes you make most often. Fix those this week. Next week, pick two more. You do not need to be perfect. You just need to be a little better than yesterday, the same way a cook slowly learns to keep the gas low and the knife sharp.
Quick recap
- Do not block async code with
.Resultor.Wait(). Useawaitall the way up. - Avoid
async voidexcept for event handlers. - Use
ConfigureAwait(false)in library code; you do not need it in ASP.NET Core. - Use
IHttpClientFactoryinstead of creating a newHttpClienteach time. - Fix EF Core N+1 with
Include, and useAsNoTracking()for read-only queries. - Use
StringBuilderfor string building in loops. - Wrap
IDisposableresources inusing. - Never swallow exceptions silently; log or rethrow.
- Keep nullable warnings on and handle null instead of forcing it with
!. - Keep secrets out of
appsettings.json; use user secrets and vaults. - Move slow work out of the request into a background job.
- Prefer built-in tools; check licenses (MediatR and MassTransit are now commercial).
- Support
CancellationTokenin long-running async work. - Always measure before you optimize.
References and further reading
- Async/Await Best Practices (Microsoft Learn)
- Nullable reference types (Microsoft Learn)
- Working with nullable reference types in EF Core
- Common Async/Await mistakes in C# (DEV Community)
- How Async/Await Works Internally and Common Developer Mistakes (C# Corner)
Related Posts
SOLID Principles in C# and .NET: A Beginner-Friendly Guide
Learn the 5 SOLID principles in C# and .NET with simple words, real-life examples, diagrams, and clean code you can copy and try yourself today.
How to Write Better and Cleaner Code in .NET
A beginner-friendly guide to writing better, cleaner C# and .NET code using clear names, small methods, modern C# 14 features, and simple structure.
Best Practices for Increasing Code Quality in .NET Projects
Beginner-friendly guide to raising code quality in .NET with analyzers, EditorConfig, nullable types, tests, and CI checks that catch bugs early.
Mastering Exception Handling in C#: A Comprehensive Guide
Learn C# exception handling the friendly way: try, catch, finally, custom exceptions, filters, throw vs throw ex, and real best practices for .NET 10.
Getting Started With MongoDB in EF Core: A Beginner's Guide
A friendly beginner guide to using MongoDB with EF Core in .NET. Learn setup, DbContext, UseMongoDB, CRUD, mapping, and the limits you must know.
5 Awesome C# Refactoring Tips to Write Cleaner Code
Learn 5 simple C# refactoring tips with examples in modern C# 14. Make your code cleaner, safer, and easier to read using guard clauses and more.