Checked and Unchecked Contexts in C# Arithmetic

In C#, the checked and unchecked contexts control how the runtime handles integral arithmetic overflow and underflow. When arithmetic operations exceed the storage boundaries of a data type, C# can either throw an exception to prevent silent data corruption or silently wrap the value around for optimal performance. This article explains how both contexts function, their syntax as keywords and statements, the compiler-level defaults governing them, and best practices for their use.

The Problem: Integer Overflow

Integer types like int, long, and byte have fixed bit sizes. For example, a standard 32-bit signed integer (int) has a maximum value of 2,147,483,647 (int.MaxValue). If you add 1 to this value without safeguards, the binary representation rolls over into negative territory, resulting in -2,147,483,648.

This rollover behavior can introduce subtle logic bugs, infinite loops, and security vulnerabilities unless handled intentionally.

The checked Context

The checked context enforces strict bounds checking at runtime. If an integral arithmetic operation (such as addition, subtraction, multiplication, or conversion) exceeds the minimum or maximum limit of the target type, an OverflowException is thrown immediately.

You can apply the checked context as either an expression or a block statement.

As an Expression

int max = int.MaxValue;
int result = checked(max + 1); // Throws System.OverflowException

As a Block Statement

checked
{
    int a = int.MaxValue;
    int b = 10;
    int c = a + b; // Throws System.OverflowException
}

The checked context only applies to integral types (int, uint, short, byte, long, etc.) and explicit conversions between integral types. It does not apply to floating-point types (float, double), which produce Infinity or NaN instead of throwing exceptions.

The unchecked Context

The unchecked context suppresses overflow checking. When an operation exceeds bounds inside an unchecked context, the bits are truncated, and the value wraps around without raising any exceptions.

As an Expression

int max = int.MaxValue;
int result = unchecked(max + 1); // Returns -2,147,483,648

As a Block Statement

unchecked
{
    int a = int.MaxValue;
    int b = 1;
    int c = a + b; // Returns -2,147,483,648
}

Compiler Defaults and Behavior

C# employs specific defaults depending on whether the code is evaluated at compile time or runtime:

  1. Runtime Operations: By default, non-constant arithmetic operations in C# execute within an unchecked context. This minimizes CPU instruction overhead, prioritizing performance.
  2. Compile-Time Constants: Constant expressions are evaluated at compile time in a checked context by default. For example, int x = int.MaxValue + 1; causes a compile-time error (CS0220: The operation overflows at compile time in checked mode) unless explicitly wrapped in unchecked(int.MaxValue + 1).
  3. Project-Wide Configuration: You can change the runtime default across an entire project by adding <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow> to your .csproj file (equivalent to passing the /checked flag to the compiler). When enabled, all non-constant arithmetic is treated as checked unless explicitly wrapped in an unchecked block.

Key Differences

Feature checked unchecked
Overflow Behavior Throws System.OverflowException Truncates bits (wraps around)
Default Runtime State Must be explicitly declared (or enabled via compiler flag) Default behavior for non-constant code
Performance Slightly slower due to CPU overflow flag checks Fastest execution (raw CPU instructions)
Primary Use Case Financial, safety, and business-critical math Bitwise manipulation, hashing, performance loops

When to Use Each Context

  • Use checked when calculation accuracy is critical to the integrity of the application. This includes calculating monetary values, index bounds, and counters where silent truncation would lead to incorrect business decisions or corrupted persistent data.
  • Use unchecked when deliberate overflow is part of the algorithm, such as generating hash codes (e.g., overriding GetHashCode()), cryptography routines, or low-level bit shifting. It is also useful for overriding a project-wide <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow> setting in isolated, performance-sensitive algorithms.