Skip to main content
SEMastery
Fundamentalsbeginner

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.

11 min readUpdated October 5, 2025

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

Habit
Works today
Grows under load
Breaks in production
Late-night fix

Steps

1

Habit

A small bad pattern

2

Works today

Tests pass locally

3

Grows under load

Real traffic arrives

4

Breaks

Slow, crash, or leak

5

Late fix

Stressful debugging

The same chain repeats for almost every pitfall in this list.

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();
}
Why blocking on async causes a deadlock

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 areUse ConfigureAwait(false)?
Shared library codeYes
ASP.NET Core controllersNot needed
UI code that updates the screenNo
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

Register AddHttpClient
Inject IHttpClientFactory
CreateClient()
Reused connection

Steps

1

Register

AddHttpClient in Program.cs

2

Inject

IHttpClientFactory

3

Create

factory.CreateClient()

4

Reuse

No socket leak

Let the factory manage connections so sockets are reused.

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 time

Fix 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();
N+1 versus a single Include query

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 typeTracking choice
You will edit and saveDefault (tracked)
Read-only list or reportAsNoTracking()
Big exportAsNoTracking() + 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, automatically

This 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

Value may be null
Check or ?.
Provide default
Safe code

Steps

1

May be null

Compiler warns you

2

Check

if or ?. operator

3

Default

?? fallback value

4

Safe

No NRE at runtime

Always have a plan for null instead of forcing it away.

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.

Reply fast, finish the slow work in the background

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.

StepWhat to do
1. MeasureFind the real slow spot
2. Fix one thingChange the biggest cost
3. Measure againConfirm it helped
4. StopDo not over-tune
The measure-first loop for performance work

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 .Result or .Wait(). Use await all the way up.
  • Avoid async void except for event handlers.
  • Use ConfigureAwait(false) in library code; you do not need it in ASP.NET Core.
  • Use IHttpClientFactory instead of creating a new HttpClient each time.
  • Fix EF Core N+1 with Include, and use AsNoTracking() for read-only queries.
  • Use StringBuilder for string building in loops.
  • Wrap IDisposable resources in using.
  • 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 CancellationToken in long-running async work.
  • Always measure before you optimize.

References and further reading

Related Posts