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.
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.
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.
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
- Create:
dotnet new console -n AssertDemo
- Navigate:
cd AssertDemo
- Edit Program.cs
- Update .csproj
- Run:
dotnet run
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];
}
}
<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.