C#  

Common C# Mistakes Even Senior Developers Make

1. Overusing async / await Without Understanding the Cost

The Mistake

Using async/await everywhere—even when no real async work is happening.

public async Task<int> GetCountAsync()
{
    return 10;
}

Why It’s a Problem

  • Adds unnecessary state machines

  • Slight performance overhead

  • Misleads future readers into thinking I/O is involved

Better Approach

Only use async when awaiting actual asynchronous work.

public Task<int> GetCountAsync()
{
    return Task.FromResult(10);
}

Rule of thumb: Async all the way—but only when async is actually needed.

2. Ignoring IDisposable and Resource Lifetimes

The Mistake

Forgetting to properly dispose of unmanaged resources.

var stream = new FileStream("data.txt", FileMode.Open);
// use stream

Why It’s a Problem

  • File locks

  • Memory pressure

  • Resource leaks under load

Correct Usage

using var stream = new FileStream("data.txt", FileMode.Open);
// use stream

Or (when async):

await using var stream = new FileStream("data.txt", FileMode.Open);

Senior pitfall: “The GC will clean it up” — not fast enough, not safely.

3. Misusing LINQ in Performance-Critical Code

The Mistake

Writing expressive LINQ without considering allocations and execution cost.

var result = users
    .Where(u => u.IsActive)
    .Select(u => u.Email)
    .ToList();

Why It’s a Problem

  • Multiple enumerations

  • Hidden allocations

  • Harder debugging

When It Matters

  • Hot paths

  • Large collections

  • High-throughput services

Alternative

var result = new List<string>();
foreach (var user in users)
{
    if (user.IsActive)
        result.Add(user.Email);
}

Balance readability with performance, not blindly one or the other.

4. Overengineering With Abstractions

The Mistake

Introducing interfaces and layers too early.

IUserService → IUserRepository → IUserDataProvider → IUserStorage

Why It’s a Problem

  • Increased cognitive load

  • Harder debugging

  • Slower development

Better Principle

Abstract when there is real variation or proven need.

Ask:

  • Is this likely to change?

  • Do I have multiple implementations today?

If not, keep it simple.

5. Forgetting That DateTime Is Tricky

The Mistake

Using DateTime.Now everywhere.

var expiresAt = DateTime.Now.AddDays(1);

Why It’s a Problem

  • Time zones

  • Daylight saving changes

  • Inconsistent behavior across systems

Better Approach

Prefer DateTime.UtcNow or DateTimeOffset.

var expiresAt = DateTimeOffset.UtcNow.AddDays(1);

Time bugs are notorious and often discovered in production.

6. Catching Exception Too Broadly

The Mistake

try
{
    DoSomething();
}
catch (Exception ex)
{
    Log(ex);
}

Why It’s Dangerous

  • Swallows critical exceptions

  • Hides real bugs

  • Makes systems fail silently

Better Approach

Catch specific exceptions and handle intentionally.

catch (IOException ex)
{
    Log(ex);
    throw;
}

Senior rule: If you catch it, you must own it.

7. Not Understanding Value vs Reference Semantics

The Mistake

Assuming structs behave like classes.

public struct Counter
{
    public int Value;
}

Passing it around and expecting mutations to persist.

Why It’s a Problem

  • Silent bugs

  • Unexpected copies

  • Performance issues

Key Reminder

  • struct → value type (copied)

  • class → reference type

Use structs only when:

  • Small

  • Immutable

  • Performance-critical

8. Skipping Meaningful Logging

The Mistake

_logger.LogError("Something went wrong");

Why It’s Useless

  • No context

  • No data

  • No way to reproduce

Better Logging

_logger.LogError(ex, "Failed to process order {OrderId}", orderId);

Logs are for future you at 3 AM.