C# & .NET (C# 14, .NET 10 LTS) Module 2 – Types, variables and operators
Integers, overflow and checked arithmetic
Pick the C# integer type that fits your range, see int.MaxValue + 1 wrap to a negative number, and stop it with checked arithmetic before data goes wrong.
What you will learn
- Pick the integer type, from byte to Int128, whose range fits the values a program stores
- Predict the wrapped result of an overflowing operation and catch it with checked arithmetic
- Decide where a checked context applies, for an expression, a block or a whole project
- Use integer division, the remainder and the shift operators correctly on negative numbers
- Add money as a long count of paise without a silent overflow
Before you start
What you’ll learn · Before you start
What you will learn
- Pick the integer type, from byte to Int128, whose range fits the values a program stores
- Predict the wrapped result of an overflowing operation and catch it with checked arithmetic
- Decide where a checked context applies, for an expression, a block or a whole project
- Use integer division, the remainder and the shift operators correctly on negative numbers
- Add money as a long count of paise without a silent overflow
Before you start
On this page
In C#, int.MaxValue + 1 is -2,147,483,648: the sum wraps around to the smallest int, and the program carries on
without an error. Thirty orders of 1,000,000.00 each, counted in hundredths in an int, add up to -1,294,967,296
the same way.
C# integers come in fixed sizes, from the 8 bits of a byte to the 128 bits of Int128, and each has a smallest
and a largest value. You will pick a type from the range its values reach, watch what happens one step past the edge,
and turn that silent wraparound into an exception with checked arithmetic.
Each integer type holds a fixed range of whole numbers
An integer type stores a whole number in a fixed number of bits, and the bits decide its range. This program prints the smallest and largest value of each type, then the type that the compiler gives to some literals:
using System.Globalization;
using System.Runtime.CompilerServices;
// Every number prints with the invariant culture, the same on every computer.
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
Console.WriteLine("Type Bits Smallest to largest");
Row("sbyte", 8, sbyte.MinValue, sbyte.MaxValue);
Row("byte", 8, byte.MinValue, byte.MaxValue);
Row("short", 16, short.MinValue, short.MaxValue);
Row("ushort", 16, ushort.MinValue, ushort.MaxValue);
Row("int", 32, int.MinValue, int.MaxValue);
Row("uint", 32, uint.MinValue, uint.MaxValue);
Row("long", 64, long.MinValue, long.MaxValue);
Row("ulong", 64, ulong.MinValue, ulong.MaxValue);
Row("nint", nint.Size * 8, nint.MinValue, nint.MaxValue);
Console.WriteLine($"Int128 has 128 bits; its largest value is {Int128.MaxValue:N0}");
Console.WriteLine();
Console.WriteLine("Literal Value Type");
Literal(0x7FFF_FFFF);
Literal(0b1111_1111);
Literal(1_00_000);
Literal(3_000_000_000);
Literal(3_000_000_000L);
Literal(100U);
Literal(100UL);
static void Row<T>(string name, int bits, T min, T max)
where T : IFormattable =>
Console.WriteLine($"{name,-7} {bits,4} {min:N0} to {max:N0}");
// CallerArgumentExpression hands the method the literal exactly as it is written.
static void Literal<T>(T value, [CallerArgumentExpression(nameof(value))] string text = "") =>
Console.WriteLine($"{text,-16} {value,-14} {typeof(T).Name}"); Output
Type Bits Smallest to largest sbyte 8 -128 to 127 byte 8 0 to 255 short 16 -32,768 to 32,767 ushort 16 0 to 65,535 int 32 -2,147,483,648 to 2,147,483,647 uint 32 0 to 4,294,967,295 long 64 -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 ulong 64 0 to 18,446,744,073,709,551,615 nint 64 -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 Int128 has 128 bits; its largest value is 170,141,183,460,469,231,731,687,303,715,884,105,727 Literal Value Type 0x7FFF_FFFF 2147483647 Int32 0b1111_1111 255 Int32 1_00_000 100000 Int32 3_000_000_000 3000000000 UInt32 3_000_000_000L 3000000000 Int64 100U 100 UInt32 100UL 100 UInt64
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run ranges.cs
The signed types, sbyte, short, int and long, spend one bit on the sign, so int stops at 2,147,483,647
while uint, with no sign, reaches 4,294,967,295. nint has the size of a pointer in the process that runs the
program, 64 bits here. A literal can be hexadecimal (0x) or binary (0b), an underscore is only a separator for
the reader, so 1_00_000 is 100,000, and a suffix picks the type: L for long, U for uint, UL for ulong.
Choose the type by the largest value the program can ever store:
intfor counts and indexes, the everyday choice: Microsoft’s conventions prefer it to the unsigned types.longwhen values can pass 2.1 billion, such as file sizes in bytes or money counted in its smallest unit.bytefor the raw bytes of files and network messages.Int128, new in .NET 7, only for values pastlong’s 9.2 × 10¹⁸.
Checkpoint
Check: Integer types and their ranges
A result that does not fit wraps around, unless the code is checked
When a result is too big for its type, C# keeps the low bits that fit and drops the rest. Step through 127 + 1 in an
sbyte, whose 8 bits make the arithmetic easy to follow:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The largest sbyte. An sbyte has 8 bits, and the top one is the sign bit. 127, the largest sbyte, is a 0 for the sign followed by seven 1s.
- Add 1. Adding 1 turns each of the seven 1s into a 0 and carries 1 to the left, the way 999 + 1 carries in decimal, until the carry reaches the sign bit.
- The carry flips the sign. The result is 1000 0000. Unchecked arithmetic keeps whatever 8 bits come out, so the carry lands in the sign bit and no error is raised.
- Read back as -128. Read as a signed number, 1000 0000 is -128: the sum has wrapped around to the smallest sbyte. wrap.cs printed exactly that, and an int does the same at 2,147,483,647.
- In a checked context. In a checked context the runtime tests whether the true sum, 128, fits in an sbyte. It does not, so it throws OverflowException and the variable keeps its old value.
Signed integers use two's complement: The way .NET stores signed integers in binary: the top bit counts as minus 2 to the power of the bits below it, so 1000 0000 in an sbyte is -128, and one past the largest value reads back as the smallest., which is why one past the largest value
reads back as the smallest. An int does the same at its edge:
using System.Globalization;
// Every number prints with the invariant culture, the same on every computer.
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
// One past the largest value of a type wraps around to its smallest, with no error.
int max = int.MaxValue;
int next = max + 1;
Console.WriteLine($"int.MaxValue + 1 = {next:N0}");
sbyte small = 127;
small++;
Console.WriteLine($"127 in an sbyte, plus 1 = {small}");
byte full = 255;
full++;
Console.WriteLine($"255 in a byte, plus 1 = {full}");
uint none = 0;
none--;
Console.WriteLine($"0 in a uint, minus 1 = {none:N0}");
// 30 orders of 1,000,000.00 each, counted in hundredths (cents or paise).
int order = 100_000_000;
int totalInt = 0;
long totalLong = 0;
for (int i = 0; i < 30; i++)
{
totalInt += order;
totalLong += order;
}
Console.WriteLine($"30 orders, int total: {totalInt:N0}");
Console.WriteLine($"30 orders, long total: {totalLong:N0}");
// checked turns the same overflow into an exception.
try
{
int checkedNext = checked(max + 1);
Console.WriteLine(checkedNext);
}
catch (OverflowException e)
{
Console.WriteLine($"checked(max + 1): {e.GetType().Name}: {e.Message}");
} Output
int.MaxValue + 1 = -2,147,483,648 127 in an sbyte, plus 1 = -128 255 in a byte, plus 1 = 0 0 in a uint, minus 1 = 4,294,967,295 30 orders, int total: -1,294,967,296 30 orders, long total: 3,000,000,000 checked(max + 1): OverflowException: Arithmetic operation resulted in an overflow.
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run wrap.cs
The order totals are the realistic case: 30 orders of 100,000,000 hundredths make 3,000,000,000, which no int can
hold, so the int total came out negative while the long total is right. Microsoft’s documentation describes the
same edge in Environment.TickCount, the milliseconds since the computer started: it climbs for about 24.9 days to
int.MaxValue, then jumps to int.MinValue.
This silent wrap is integer overflow: A result too large or too small for its integer type. By default C# keeps the low bits that fit, so the value wraps around without an error; in a checked context the operation throws OverflowException instead., and it is the default, because integer
arithmetic in C# is unchecked unless you ask otherwise. Checked arithmetic is the other choice: inside checked(…)
the same addition threw OverflowException, as the last line shows, so a wrong total stops the program instead of
reaching a report. Four tools decide what happens at the edge of a type:
checked, as a block or an expression, turns integer overflow inside it into anOverflowException.uncheckedasks for wrapping on purpose, for example in a hash function.- The project setting
CheckForOverflowUnderflowmakes checked arithmetic the default for every line of a project. - A wider type, such as
longforint, moves the edge beyond any value the program will meet.
Integer division and the remainder have edges of their own, with negative numbers and with zero.
Programmer Calculator (Bitwise) Choose signed 32-bit, type 2147483647 + 1 and see the result wrap to -2,147,483,648.Key idea
Integer arithmetic in C# wraps around without a word when a result does not fit, so give each value a type wide enough for it, and use checked arithmetic wherever a wrapped result would be wrong.
Checkpoint
Check: Wraparound and checked arithmetic
Checked and unchecked contexts, reproduced in code Pro: opens with a Pro, Premium or Ultimate pass
A checked context covers less code than many people expect, and conversions follow rules of their own. The runs below map its edges line by line, switch checking on for a whole project and show the instruction it compiles to.
In this part:
- What a checked context covers, and what it leaves out
- Turning checking on for a whole project, with a session
- What checked changes in the compiled code
4 runnable examples (C#, Python) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Division, remainders, shifts and money in long, with the numbers Pro: opens with a Pro, Premium or Ultimate pass
Integer division, the remainder and the shifts follow simple rules that turn into bugs once a number is negative. The runs below reproduce three such bugs, then add money as whole paise with a sum that cannot wrap.
In this part:
- Which way integer division rounds, and the sign of a remainder
- Right shifts of a negative number, and shift counts past 31
- Money as a long count of paise, with a sum that names the item that overflowed
3 runnable examples (C#) · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Practise: totals that cannot wrap
Make the order total stop instead of wrapping
Copy wrap.cs to your computer, put the loop inside checked { … } and make
Console.WriteLine(i + 1); the loop’s first line. Before you run it, work out on which order the int total
throws, then run it with dotnet run wrap.cs: the last number it prints is that order.
Then try the questions on ranges and wraparound, with the idea you are less sure of first:
Practice that adapts to you
Practise this lesson
9 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · What happens when an int overflows in C#?
By default nothing visible: integer arithmetic in C# is unchecked, so the result keeps only the low 32 bits and wraps
around. int.MaxValue + 1 gives int.MinValue, -2,147,483,648, without an exception, and 30 orders of
100,000,000 hundredths summed in an int come out negative. In a checked context, a checked block or expression, or
a project built with CheckForOverflowUnderflow, the same operation throws OverflowException. The defence is a type
wide enough for every value, such as long for money in paise, plus checked arithmetic where a wrapped value would be
wrong.
-
Fresher to mid
A nightly job adds up a merchant's sales in paise in an int, and one morning the report shows a negative total for the day. Explain what happened, how you would find which days are wrong, and how you would fix the code and the stored totals.
-
Mid to senior
After an overflow bug reached production, your team proposes turning on CheckForOverflowUnderflow for the whole solution. What exactly would it change, what can break or slow down, and where would you still write unchecked?
-
Fresher to mid
A hand-written hash table computes bucket = key.GetHashCode() % buckets.Length and now and then crashes with IndexOutOfRangeException. Explain why it happens only sometimes, and write the bucket calculation correctly.
Key takeaways
- Each C# integer type has a fixed size and range:
intstops at 2,147,483,647,longat about 9.2 × 10¹⁸. - A literal’s value and suffix decide its type, and an underscore in a literal only helps the reader.
- By default a result that does not fit wraps around without an error, as
int.MaxValue + 1becameint.MinValue. - Checked arithmetic, a
checkedblock or expression, turns that overflow into anOverflowException.
Terms in this lesson
- integer overflow
- A result too large or too small for its integer type. By default C# keeps the low bits that fit, so the value wraps around without an error; in a checked context the operation throws OverflowException instead.
- two's complement
- The way .NET stores signed integers in binary: the top bit counts as minus 2 to the power of the bits below it, so 1000 0000 in an sbyte is -128, and one past the largest value reads back as the smallest.
References
- Integral numeric types (Microsoft Learn)
- The checked and unchecked statements (Microsoft Learn)
- Arithmetic operators (integer division, remainder, overflow) (Microsoft Learn)
- Compiler options: language feature rules (CheckForOverflowUnderflow) (Microsoft Learn)
- Bitwise and shift operators (Microsoft Learn)
- Int128 struct (Microsoft Learn)
- Environment.TickCount property (Microsoft Learn)
- .NET coding conventions (int rather than unsigned types) (Microsoft Learn)
- Floating point-to-integer conversions are saturating (.NET 9) (Microsoft Learn)
- ECMA-335: Common Language Infrastructure (add.ovf, conv.ovf) (Ecma International)
- Math.DivRem method (Microsoft Learn)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress