The other day, I played around with the Fibonacci sequence and overflowed my int variables in surprisingly low iterations. Did you know you’ll overflow an integer in **48 ** iterations? Don’t believe me? Let’s take a look at the result of the code you’ll see later in this post.

Enter an integer: 48
Fibonacci sequence for 48:
0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368,75025,121393,196418,
317811,514229,832040,1346269,2178309,3524578,5702887,9227465,14930352,24157817,39088169,63245986,102334155,165580
141,267914296,433494437,701408733,1134903170,1836311903,-1323752223

You’ll notice that the last value has looped back to a negative value. Oh no! This is a tell-tale sign that an overflow has occurred.

This post will explore how to keep your applications safe from these overflow issues.

The code causing the overflow

First, let’s look at my implementation of the Fibonacci sequence generator. I’m using Spectre.Console to make the output more appealing, but it’s optional. I’m also using the System.Numerics namespace, which will allow you to experiment with different number types in .NET.

using System.Numerics;
using Spectre.Console;

while (true)
{
    try
    {
        var integer = AnsiConsole.Ask<int>("Enter an integer: ");

        if (integer <= -1)
        {
            AnsiConsole.MarkupLine("Goodbye!");
            break;
        }

        var numbers = GenerateFibonacci<int>((uint)integer);
        AnsiConsole.MarkupLine($"[bold green]Fibonacci sequence for {integer}:[/]");
        AnsiConsole.MarkupLine($"[bold yellow]{string.Join(",", numbers)}[/]");
        AnsiConsole.MarkupLine("");
    }
    catch (ArgumentOutOfRangeException)
    {
        AnsiConsole.MarkupLine("[red]Error: pick a value greater than 2[/]");
    }
}

static T[] GenerateFibonacci<T>(uint iterations)
    where T : INumber<T>
{
    ArgumentOutOfRangeException
        .ThrowIfLessThan<uint>(iterations, 2, "iterations must be greater than or equal to 2");

    T[] fib = new T[iterations];
    fib[0] = T.Zero;
    fib[1] = T.One;
    for (int i = 2; i < iterations; i++)
    {
        fib[i] = fib[i - 1] + fib[i - 2];
    }

    return fib;
}

Neat! Now, why does the issue occur in the first place?

Unchecked Arithmetic

By default, .NET performs unchecked arithmetic operations. That means the runtime assumes you know what you’re doing when adding one variable to another. This is true in most day-to-day cases, as the scale of numbers most developers deal with is on the lower side of the maximum int and long values.

For integers, the max value is 2147483647, and it is doubled to 4294967295 if you drop the sign —/+ from the value. For a long, the max value is 9223372036854775807, which is quite a lot bigger but not infinite. If your application increments values indefinitely, you will run into overflow issues, and no known type will save you. It’s just math!

As you’ve seen previously, the issue is that the numbers will loop around from positive to negative values and back. Your application will continue to “work” but likely will be nonsensical and wrong.

How do we fix this issue? Well, there are two ways.

The checked keyword

The first and most precise way to fix this issue is to use the checked keyword. Using this keyword creates a scope where arithmetic overflows will trigger an OverflowException.

static T[] GenerateFibonacci<T>(uint iterations)
    where T : INumber<T>
{
    ArgumentOutOfRangeException
        .ThrowIfLessThan<uint>(iterations, 2, "iterations must be greater than or equal to 2");

    T[] fib = new T[iterations];
    fib[0] = T.Zero;
    fib[1] = T.One;
    for (int i = 2; i < iterations; i++)
    {
        checked
        {
            fib[i] = fib[i - 1] + fib[i - 2];
        }
    }

    return fib;
}

Rerunning the same code results in the following exception.

Enter an integer: 49
Unhandled exception. System.OverflowException: Arithmetic operation resulted in an overflow.
   at System.Int32.System.Numerics.IAdditionOperators<System.Int32,System.Int32,System.Int32>.op_CheckedAddition(Int32 left, Int32 right)
   at Program.<<Main>$>g__GenerateFibonacci|0_0[T](UInt32 iterations) in /Users/khalidabuhakmeh/RiderProjects/zed-sample/Program.cs:line 40
   at Program.<Main>$(String[] args) in /Users/khalidabuhakmeh/RiderProjects/zed-sample/Program.cs:line 16

Nice. This technique is very selective and changes the intermediate language to call the op_CheckedAddition method instead.

      // [40 13 - 40 46]
      IL_003f: ldloc.0      // fib
      IL_0040: ldloc.1      // i
      IL_0041: ldloc.0      // fib
      IL_0042: ldloc.1      // i
      IL_0043: ldc.i4.1
      IL_0044: sub.ovf
      IL_0045: ldelem       !!0/*T*/
      IL_004a: ldloc.0      // fib
      IL_004b: ldloc.1      // i
      IL_004c: ldc.i4.2
      IL_004d: sub.ovf
      IL_004e: ldelem       !!0/*T*/
      IL_0053: constrained. !!0/*T*/
      IL_0059: call         !2/*T*/ class [System.Runtime]System.Numerics.IAdditionOperators`3<!!0/*T*/, !!0/*T*/, !!0/*T*/>::op_CheckedAddition(!0/*T*/, !1/*T*/)
      IL_005e: stelem       !!0/*T*/

Cool! Problem solved, but what if we want all our arithmetic checked?

Project Setting for Checked Arithmetic

You can add the CheckForOverflowUnderflow property in your project settings, and it will change all supported arithmetic operations to use checked operations.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Spectre.Console" Version="0.49.1" />
  </ItemGroup>

</Project>

Easy peasy. Running the original code results in the same System.OverflowException.

To opt out of checking, you can use the converse unchecked keyword.

static T[] GenerateFibonacci<T>(uint iterations)
    where T : INumber<T>
{
    ArgumentOutOfRangeException
        .ThrowIfLessThan<uint>(iterations, 2, "iterations must be greater than or equal to 2");

    T[] fib = new T[iterations];
    fib[0] = T.Zero;
    fib[1] = T.One;
    for (int i = 2; i < iterations; i++)
    {
        unchecked
        {
            fib[i] = fib[i - 1] + fib[i - 2];
        }
    }

    return fib;
}

Conclusion

We can sometimes take for granted arithmetic operations and assume they will work. In most cases, they will, but if you’re building mission-critical applications that could run into underflow and overflow situations, it’s better to check these values so as not to cause potential headaches down the line.

I hope you enjoyed this post, and as always, thanks for reading.