assert in C#: Catch Bugs Early, Document Intent

Catching Logic Errors Early

If you've ever discovered a bug hours after it was introduced because you didn't notice invalid state until much later, you've felt the pain of delayed error detection. Assertions catch problems the moment assumptions break instead of letting corrupted state propagate through your system.

Debug.Assert checks conditions you expect to always be true during development. If a condition fails, the assertion halts execution and shows you exactly where your assumptions were wrong. These checks compile out of release builds, giving you validation during development without production overhead.

You'll add assertions to a data processing pipeline that catches invalid states immediately. By the end, you'll know when assertions clarify code and when they hide behind false confidence.

Using Debug.Assert

Call Debug.Assert with a condition and an optional message. If the condition is false, the debugger breaks and shows your message. This stops execution right where the problem occurs.

BasicAssert.cs
using System.Diagnostics;

public class OrderProcessor
{
    public void ProcessOrder(Order order)
    {
        Debug.Assert(order != null, "Order cannot be null");
        Debug.Assert(order.Items.Count > 0, "Order must have at least one item");
        Debug.Assert(order.Total >= 0, "Order total cannot be negative");

        foreach (var item in order.Items)
        {
            Debug.Assert(item.Quantity > 0, "Item quantity must be positive");
            Debug.Assert(item.Price >= 0, "Item price cannot be negative");
        }

        Console.WriteLine($"Processing order with {order.Items.Count} items");
    }
}

public class Order
{
    public List<OrderItem> Items { get; set; } = new();
    public decimal Total { get; set; }
}

public class OrderItem
{
    public int Quantity { get; set; }
    public decimal Price { get; set; }
}

Each assertion documents a precondition or invariant. If any fails during development, you'll see it immediately. These checks disappear in release builds, so production code runs without the overhead of constant validation.

Common Assertion Patterns

Use assertions to validate method preconditions, postconditions, and class invariants. Check that parameters are in expected ranges, return values meet contracts, and object state remains valid.

AssertPatterns.cs
using System.Diagnostics;

public class BankAccount
{
    private decimal _balance;

    public decimal Balance
    {
        get => _balance;
        private set
        {
            Debug.Assert(value >= 0, "Balance cannot be negative");
            _balance = value;
        }
    }

    public void Deposit(decimal amount)
    {
        Debug.Assert(amount > 0, "Deposit amount must be positive");

        var oldBalance = Balance;
        Balance += amount;

        Debug.Assert(Balance == oldBalance + amount,
            "Balance should increase by deposit amount");
    }

    public void Withdraw(decimal amount)
    {
        Debug.Assert(amount > 0, "Withdrawal amount must be positive");
        Debug.Assert(Balance >= amount, "Insufficient funds for withdrawal");

        var oldBalance = Balance;
        Balance -= amount;

        Debug.Assert(Balance == oldBalance - amount,
            "Balance should decrease by withdrawal amount");
        Debug.Assert(Balance >= 0, "Balance invariant violated");
    }
}

The precondition assertions check inputs. The postcondition assertions verify that operations produced expected results. The invariant assertion ensures the balance stays non-negative throughout. These checks catch bugs in your logic, not in user input.

Assertions Versus Exceptions

Assertions check programmer errors. Exceptions handle runtime errors. Use assertions for conditions that should never happen if your code is correct. Use exceptions for external failures like network errors or invalid user input.

AssertVsException.cs
using System.Diagnostics;

public class DataValidator
{
    public void ProcessUserInput(string input)
    {
        if (string.IsNullOrEmpty(input))
            throw new ArgumentException("Input cannot be empty", nameof(input));

        var processed = ProcessInternal(input);

        Debug.Assert(processed != null,
            "ProcessInternal should never return null");
    }

    private string? ProcessInternal(string input)
    {
        Debug.Assert(!string.IsNullOrEmpty(input),
            "Input should be validated before calling ProcessInternal");

        return input.ToUpperInvariant();
    }
}

The ArgumentException handles external input that might be invalid. The assertions check internal logic that should always be correct. Assertions document your assumptions. Exceptions defend against things outside your control.

Try It Yourself

Add assertions to a stack implementation to catch underflow and overflow bugs during development. This shows how assertions guard against programmer errors.

Steps

  1. Create: dotnet new console -n AssertDemo
  2. Navigate: cd AssertDemo
  3. Edit Program.cs
  4. Update .csproj
  5. Run: dotnet run
Program.cs
using System.Diagnostics;

var stack = new BoundedStack<int>(3);

stack.Push(1);
stack.Push(2);
stack.Push(3);

Console.WriteLine($"Popped: {stack.Pop()}");
Console.WriteLine($"Popped: {stack.Pop()}");
Console.WriteLine($"Count: {stack.Count}");

class BoundedStack<T>
{
    private readonly T[] _items;
    private int _count;

    public int Count
    {
        get
        {
            Debug.Assert(_count >= 0 && _count <= _items.Length,
                "Count invariant violated");
            return _count;
        }
    }

    public BoundedStack(int capacity)
    {
        Debug.Assert(capacity > 0, "Capacity must be positive");
        _items = new T[capacity];
    }

    public void Push(T item)
    {
        Debug.Assert(_count < _items.Length, "Stack overflow");
        _items[_count++] = item;
    }

    public T Pop()
    {
        Debug.Assert(_count > 0, "Stack underflow");
        return _items[--_count];
    }
}
AssertDemo.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

Output

Popped: 3
Popped: 2
Count: 1

Best Practices

Write assertions that clearly state what you expect to be true. Use messages that explain why the condition matters, not just what the condition is. Future developers reading your assertions should understand the invariant you're enforcing.

Place assertions at the beginning of methods to check preconditions and at the end to verify postconditions. Assert class invariants after any operation that modifies state. This creates a contract that documents how your code should behave.

Never put side effects in assertion conditions. Since assertions compile out in release builds, any side effect will disappear. Keep assertion conditions pure, with no method calls that change state or have observable effects.

Combine assertions with unit tests. Tests verify behavior with expected inputs. Assertions catch unexpected states during normal development. Use both for comprehensive validation. Tests run in isolation, assertions check real usage patterns.

Frequently Asked Questions

When should I use Debug.Assert vs throwing exceptions?

Use assertions for conditions that should never happen if your code is correct. Use exceptions for runtime errors from external inputs or environment. Assertions disappear in release builds. Exceptions stay and handle recoverable errors. Assert checks programmer assumptions, exceptions handle user errors.

What happens when an assertion fails?

In debug builds, failed assertions show a dialog with the condition, file, and line number. Attach a debugger to break at the failure point. In release builds with DEBUG undefined, assertions compile to nothing. This is why you never use assertions for production validation.

How do assertions help with debugging?

Assertions catch bugs close to their source. Instead of noticing corruption later, you halt immediately when an invariant breaks. The assertion message documents what you expect to be true. Future developers see your assumptions explicitly rather than guessing from code.

Should I use assertions in public APIs?

No, validate public API inputs with exceptions that run in release builds. Assertions are for internal consistency checks. Public methods must defend against bad inputs in production. Use ArgumentNullException, ArgumentException, and validation that always executes.

Do assertions affect performance?

Debug.Assert has zero cost in release builds because it compiles to nothing when DEBUG isn't defined. This lets you add extensive checks during development without impacting production performance. Use assertions freely in hot paths, knowing they'll disappear in optimized builds.

Back to Articles