Using Checked and Unchecked for Overflow Control in C#

Silent Failures or Loud Crashes?

If you've ever debugged a financial calculation that produced negative balances when adding positive numbers, you've hit integer overflow. By default, C# lets arithmetic wrap around silently when values exceed type limits. int.MaxValue + 1 becomes int.MinValue without warning. This behavior is fast but dangerous in applications where correctness matters more than speed.

The checked and unchecked keywords let you control this behavior explicitly. Checked arithmetic throws OverflowException when operations exceed bounds, catching bugs immediately. Unchecked arithmetic wraps around, favoring performance when overflow is expected or acceptable. Choosing the right context for each scenario prevents both silent data corruption and unnecessary exceptions.

You'll learn when checked arithmetic prevents bugs in financial and scientific code, when unchecked arithmetic enables performance optimizations, and how to configure project-wide defaults. We'll build examples showing overflow detection, hash code generation, and patterns that balance safety with speed.

Understanding Default Overflow Behavior

C# arithmetic defaults to unchecked mode for performance. When integer operations overflow, the result wraps around using two's complement arithmetic. Adding 1 to int.MaxValue (2,147,483,647) produces int.MinValue (-2,147,483,648). This matches how the CPU hardware works but can surprise developers expecting errors.

Wrapping behavior makes sense for some scenarios like hash codes or circular buffers where modular arithmetic is intentional. For financial calculations, scientific computations, or resource counting, silent overflow creates bugs that are hard to detect and debug.

OverflowDemo.cs
// Default unchecked behavior
int maxValue = int.MaxValue; // 2,147,483,647
int overflow = maxValue + 1; // Wraps to -2,147,483,648

Console.WriteLine($"Max value: {maxValue}");
Console.WriteLine($"Max + 1: {overflow}");

// Multiplication overflow
int large = 1_000_000;
int product = large * large; // Overflows silently

Console.WriteLine($"1,000,000 * 1,000,000 = {product}");
// Prints: -727,379,968 (incorrect due to overflow)

// Subtraction underflow
int minValue = int.MinValue; // -2,147,483,648
int underflow = minValue - 1; // Wraps to 2,147,483,647

Console.WriteLine($"Min - 1: {underflow}");

These silent overflows create incorrect results without any indication that something went wrong. In production code handling money, measurements, or resource limits, these bugs can cause serious problems that only manifest under specific conditions when values grow large enough.

Using Checked Blocks for Safety

The checked keyword creates a context where arithmetic operations throw OverflowException instead of wrapping. This converts silent failures into immediate, catchable errors. Use checked blocks around calculations where correctness is critical and overflow represents a real error condition.

Checked arithmetic has a small performance cost (typically one additional CPU instruction per operation) but prevents data corruption. In most business applications, correctness outweighs this minimal overhead.

CheckedExample.cs
public decimal CalculateTotal(int itemCount, decimal pricePerItem)
{
    checked
    {
        // Throws OverflowException if itemCount * pricePerItem overflows
        int totalCents = (int)(itemCount * pricePerItem * 100);
        return totalCents / 100m;
    }
}

// Checked expression syntax
int SafeAdd(int a, int b)
{
    return checked(a + b); // Single expression
}

// Catching overflow
try
{
    checked
    {
        int result = int.MaxValue + 1;
        Console.WriteLine($"Result: {result}");
    }
}
catch (OverflowException ex)
{
    Console.WriteLine($"Overflow detected: {ex.Message}");
    // Handle error: log, return error code, use BigInteger, etc.
}

Checked blocks let you catch overflow early rather than discovering corrupted data later. When an overflow occurs, you can handle it appropriately: log the error, return a failure code, switch to arbitrary precision types like BigInteger, or notify users that input values are too large.

Unchecked Arithmetic for Performance

The unchecked keyword explicitly allows wrapping behavior even in projects configured for checked arithmetic. Use it for algorithms that rely on modular arithmetic like hash code generation, checksums, or cryptographic operations where wraparound is mathematically correct.

Unchecked blocks can also optimize hot paths where you've verified overflow cannot occur or where wrapping is acceptable. Always document why unchecked is safe in these cases to prevent future confusion.

UncheckedExample.cs
public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int Quantity { get; set; }

    public override int GetHashCode()
    {
        unchecked
        {
            // Hash codes intentionally use wrapping arithmetic
            int hash = 17;
            hash = hash * 31 + (Name?.GetHashCode() ?? 0);
            hash = hash * 31 + Price.GetHashCode();
            hash = hash * 31 + Quantity;
            return hash;
        }
    }
}

// Circular buffer using wraparound
public class CircularBuffer<T>
{
    private readonly T[] buffer;
    private int position;

    public CircularBuffer(int capacity)
    {
        buffer = new T[capacity];
    }

    public void Add(T item)
    {
        unchecked
        {
            // Intentional wraparound for circular indexing
            buffer[position % buffer.Length] = item;
            position++; // Let it overflow and wrap
        }
    }
}

Hash codes rely on the mathematical properties of modular arithmetic. Throwing exceptions on overflow would break hashing algorithms. Circular buffers use wraparound to loop back to the start. These are legitimate uses of unchecked arithmetic where wrapping is the correct behavior.

Project-Wide Overflow Checking

You can enable checked arithmetic project-wide using the CheckForOverflowUnderflow compiler option. This makes all arithmetic checked by default, and you opt out with explicit unchecked blocks. This approach favors safety by default, catching overflow bugs automatically unless you deliberately allow wrapping.

MyProject.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

With project-wide checking enabled, all arithmetic throws on overflow unless wrapped in unchecked blocks. This catches bugs during development and testing rather than in production. The performance impact is usually negligible compared to the safety benefits, but profile your specific workload if you have arithmetic-heavy code.

Overflow in Type Conversions

Checked contexts also affect explicit casts between numeric types. Casting a long to an int can overflow if the value exceeds int.MaxValue. Checked casts throw exceptions on overflow, while unchecked casts truncate silently.

ConversionOverflow.cs
long bigNumber = 5_000_000_000L; // Larger than int.MaxValue

// Unchecked cast: truncates without error
int truncated = (int)bigNumber;
Console.WriteLine($"Unchecked cast: {truncated}"); // 705,032,704 (wrong)

// Checked cast: throws OverflowException
try
{
    int safe = checked((int)bigNumber);
    Console.WriteLine($"Checked cast: {safe}");
}
catch (OverflowException)
{
    Console.WriteLine("Value too large for int");
}

// Safe conversion with validation
int SafeCast(long value)
{
    if (value < int.MinValue || value > int.MaxValue)
        throw new ArgumentOutOfRangeException(nameof(value));
    
    return (int)value;
}

Explicit conversions between numeric types are unchecked by default, even in checked contexts. Use checked((int)value) syntax to enforce overflow checking on casts. This prevents data loss when converting between types of different ranges.

Build Your Own Example

Create a console app demonstrating checked and unchecked arithmetic behavior with overflow detection.

Steps

  1. Scaffold: dotnet new console -n OverflowDemo
  2. Change directory: cd OverflowDemo
  3. Edit Program.cs with the code below
  4. Run: dotnet run
OverflowDemo.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>
Program.cs
Console.WriteLine("=== Default (Unchecked) Behavior ===");
int max = int.MaxValue;
int overflow = max + 1;
Console.WriteLine($"{max} + 1 = {overflow} (wrapped)");

Console.WriteLine("\n=== Checked Arithmetic ===");
try
{
    int result = checked(max + 1);
    Console.WriteLine($"Result: {result}");
}
catch (OverflowException)
{
    Console.WriteLine("OverflowException caught!");
}

Console.WriteLine("\n=== Hash Code (Unchecked) ===");
int hash = unchecked(max * 31 + 17);
Console.WriteLine($"Hash code: {hash} (wrapping is OK)");

Console.WriteLine("\n=== Safe Calculator ===");
int SafeMultiply(int a, int b)
{
    return checked(a * b);
}

try
{
    Console.WriteLine($"100 * 200 = {SafeMultiply(100, 200)}");
    Console.WriteLine($"100000 * 100000 = {SafeMultiply(100000, 100000)}");
}
catch (OverflowException)
{
    Console.WriteLine("Multiplication overflow prevented!");
}

Expected Output

=== Default (Unchecked) Behavior ===
2147483647 + 1 = -2147483648 (wrapped)

=== Checked Arithmetic ===
OverflowException caught!

=== Hash Code (Unchecked) ===
Hash code: ... (wrapping is OK)

=== Safe Calculator ===
100 * 200 = 20000
Multiplication overflow prevented!

Choosing the Right Approach

Enable project-wide checked arithmetic for applications where correctness is critical: financial systems, scientific computing, resource management, or any domain where silent overflow creates serious bugs. The performance cost is minimal compared to debugging production data corruption issues.

Use explicit unchecked blocks for specific algorithms that require wraparound: hash functions, checksums, cryptographic operations, or circular buffers. Document why unchecked is safe to prevent future developers from accidentally introducing checked blocks that break the algorithm.

For high-performance code in tight loops, measure before optimizing. Modern CPUs make overflow checking cheap, but in rare cases processing millions of values per second, unchecked arithmetic might provide measurable gains. Profile first, optimize second, and always benchmark the actual impact.

Consider arbitrary precision types like BigInteger when you need calculations that can exceed fixed-size integer limits. BigInteger automatically grows to accommodate any size value, trading performance for unlimited range. Use it for large number calculations where overflow would otherwise be inevitable.

Reader Questions

What happens when integer overflow occurs in C#?

By default, overflow wraps around silently. int.MaxValue + 1 becomes int.MinValue. Use checked blocks to throw OverflowException instead. This catches bugs where silent wrapping produces incorrect results. Enable compiler option CheckForOverflowUnderflow for project-wide checking.

When should I use unchecked even with project-wide checking enabled?

Use unchecked for hash code calculations, cryptographic operations, or intentional wraparound logic like circular buffers. These scenarios rely on modular arithmetic where wrapping is expected behavior. Unchecked blocks opt out of project-wide checking for specific code sections.

Does checked affect performance significantly?

Checked arithmetic adds overflow checks (typically one extra instruction per operation). The performance impact is minimal on modern CPUs but can matter in tight loops processing millions of values. Profile before optimizing. Most apps benefit more from correctness than micro-optimizations.

Can I use checked with floating-point types?

No. Checked and unchecked only affect integral types (int, long, byte, etc.). Floating-point types (float, double, decimal) follow IEEE 754 rules, representing overflow as Infinity and division by zero as NaN. Check for these special values explicitly if needed.

How do checked and const interact?

Constant expressions are always checked at compile time, regardless of context. const int x = int.MaxValue + 1; causes a compile error. This catches overflow in literals before runtime. Use unchecked((int)(expression)) to force compile-time wrapping if intentional.

Back to Articles