Skip to content

A float exception is a flag

Level: 201 → 301 · for anyone who has read that an illegal float operation triggers a CPU exception

One line: By default an IEEE 754 floating-point exception is not a crash or a CPU trap: the operation returns NaN, an infinity or a rounded value, a sticky status flag records what happened, the program carries on, and C's <fenv.h> lets the program read which flags are raised.

Rust in Action (Tim McNamara, Manning 2021) covers floating point in §2.3.3, "Comparing numbers", and a footnote there says that operations which are illegal or undefined for floats trigger a CPU exception. That is not what a default floating-point environment does. C is a good place to see why, because <fenv.h> lets a program read the floating-point status directly.

"Exception" means two things. In IEEE 754 it names an event, an operation whose exact result cannot be delivered, and there are five kinds: invalid operation, division by zero, overflow, underflow and inexact. The standard's default handling of each is to deliver a default result (NaN, an infinity, the rounded value), raise the matching status flag, and continue. A trap, where the CPU stops the program, happens only for an exception the program has unmasked, and in a default environment every one is masked. C17 7.6 defines the flag as a variable whose value is set, but never cleared, when an exception is raised. The flag is sticky: it stays up until the program clears it.

Five flags, read at run time

float_flags_c.c clears the flags, does one operation, and prints the result and every flag that went up:

float_flags_c.c in full — pasted here by tools/run_examples.py from the file CI runs.

/* An IEEE 754 "exception" is not a crash. By default each one sets a sticky
   status flag and the operation returns a value -- NaN, an infinity, a rounded
   result -- and the program carries on. <fenv.h> reads the flags.

   Build: cc -std=c17 -Wall -Wextra -pedantic float_flags_c.c -lm -o float_flags_c

   The operands are volatile so every operation happens at run time. With
   constant operands a compiler may work the answer out while compiling, and a
   flag raised inside the compiler is never raised in the program. */
#pragma GCC diagnostic ignored "-Wunknown-pragmas"
#pragma STDC FENV_ACCESS ON
#include <fenv.h>
#include <float.h>
#include <math.h>
#include <stdio.h>

static void show(const char *what, double result)
{
    /* Read the flags before printf gets a chance to do any arithmetic. */
    int raised = fetestexcept(FE_INVALID | FE_DIVBYZERO | FE_OVERFLOW
                              | FE_UNDERFLOW | FE_INEXACT);
    if (isnan(result))
        printf("%-18s = %-24s raised:", what, "NaN");
    else
        printf("%-18s = %-24.17g raised:", what, result);
    if (raised & FE_INVALID)   printf(" FE_INVALID");
    if (raised & FE_DIVBYZERO) printf(" FE_DIVBYZERO");
    if (raised & FE_OVERFLOW)  printf(" FE_OVERFLOW");
    if (raised & FE_UNDERFLOW) printf(" FE_UNDERFLOW");
    if (raised & FE_INEXACT)   printf(" FE_INEXACT");
    if (!raised)               printf(" (none)");
    printf("\n");
}

int main(void)
{
    volatile double minus_42 = -42.0, one = 1.0, zero = 0.0;
    volatile double tenth = 0.1, fifth = 0.2, biggest = DBL_MAX, smallest = DBL_MIN;
    double r;

    /* One operation at a time, clearing the flags before each. */
    feclearexcept(FE_ALL_EXCEPT);
    r = one + one;           show("1.0 + 1.0", r);
    feclearexcept(FE_ALL_EXCEPT);
    r = sqrt(minus_42);      show("sqrt(-42.0)", r);
    feclearexcept(FE_ALL_EXCEPT);
    r = one / zero;          show("1.0 / 0.0", r);
    feclearexcept(FE_ALL_EXCEPT);
    r = zero / zero;         show("0.0 / 0.0", r);
    feclearexcept(FE_ALL_EXCEPT);
    r = biggest * 2.0;       show("DBL_MAX * 2.0", r);
    feclearexcept(FE_ALL_EXCEPT);
    r = smallest / 3.0;      show("DBL_MIN / 3.0", r);
    feclearexcept(FE_ALL_EXCEPT);
    r = tenth + fifth;       show("0.1 + 0.2", r);

    /* Sticky: a flag stays raised until something clears it. An exact
       operation afterwards does not lower it. */
    printf("\nwithout clearing in between:\n");
    feclearexcept(FE_ALL_EXCEPT);
    r = sqrt(minus_42);
    r = one / zero;
    r = one + one;           show("... then 1.0 + 1.0", r);

    printf("\nno signal, no stop: the program is still running\n");
    return 0;
}

Verified output of float_flags_c.c — regenerated by tools/run_examples.py, never hand-typed.

1.0 + 1.0          = 2                        raised: (none)
sqrt(-42.0)        = NaN                      raised: FE_INVALID
1.0 / 0.0          = inf                      raised: FE_DIVBYZERO
0.0 / 0.0          = NaN                      raised: FE_INVALID
DBL_MAX * 2.0      = inf                      raised: FE_OVERFLOW FE_INEXACT
DBL_MIN / 3.0      = 7.4169128616906696e-309  raised: FE_UNDERFLOW FE_INEXACT
0.1 + 0.2          = 0.30000000000000004      raised: FE_INEXACT

without clearing in between:
... then 1.0 + 1.0 = 2                        raised: FE_INVALID FE_DIVBYZERO

no signal, no stop: the program is still running

The exact 1.0 + 1.0 raises nothing. Each line after it is one of the five exceptions, with the result the program got back in place of a crash.

  • FE_INVALID: sqrt(-42.0) and 0.0 / 0.0 have no real answer, so they return NaN. The program prints NaN from isnan rather than passing NaN to printf, because glibc on x86-64 prints this NaN as -nan and Apple's libc on the x86-64 Mac prints nan, so the text would differ between the machines.
  • FE_DIVBYZERO: 1.0 / 0.0 returns inf. In IEEE 754 this exception means an exact infinite result from finite operands. It is not an error in the answer.
  • FE_OVERFLOW: DBL_MAX * 2.0 is too big for a double, returns inf, and raises FE_INEXACT as well, because an overflowed result is never exact.
  • FE_UNDERFLOW: DBL_MIN / 3.0 is smaller than the smallest normal double, so it comes back as a subnormal number that has lost precision, with FE_INEXACT beside it.
  • FE_INEXACT: 0.1 + 0.2 had to be rounded. Almost every calculation with a fraction in it raises this one, so a program that trapped on it would stop almost at once.

The without clearing in between block is the sticky part. sqrt(-42.0) and 1.0 / 0.0 run with no clearing in between, then an exact 1.0 + 1.0, and both earlier flags are still up. A program can therefore run a whole calculation and check once at the end whether anything went wrong in it. The last line prints because nothing stopped the program.

The two pragmas and the volatile operands are there so the flags can be trusted. C17 7.6.1 says that a program which tests the flags in code translated with the FENV_ACCESS pragma off has undefined behaviour, and it leaves the default state to the implementation. So the program turns it on. Clang honours the pragma. GCC's manual says the pragma is not implemented ↗, and GCC says so with a -Wunknown-pragmas warning under -Wall. The #pragma GCC diagnostic line silences that one warning, and clang reads it too. Because GCC ignores the pragma, volatile is what holds the example to its answer on both compilers: it forces every operation to happen while the program runs, where a flag can be seen. The next section shows what happens without it.

The Build: line in the opening comment ends with -lm. <math.h> and <fenv.h> are part of standard C, but glibc ships their functions in a separate libm, and without it the link fails:

Real output — the build without -lm, GCC 13.3 and glibc 2.39, ubuntu:24.04 x86-64 in Docker; the command and the first four lines of the error
$ cc -std=c17 -Wall -Wextra -pedantic float_flags_c.c -o float_flags_c
/usr/bin/ld: /tmp/ccwtWOaV.o: in function `show':
float_flags_c.c:(.text+0x1b): undefined reference to `fetestexcept'
/usr/bin/ld: /tmp/ccwtWOaV.o: in function `main':
float_flags_c.c:(.text+0x19c): undefined reference to `feclearexcept'

On macOS those functions live in libSystem, which every program links, so the same command without -lm builds. tools/run_examples.py reads the Build: line and passes -lm on both machines.

A constant's flags never reach the program

demo/folded_flags.c is the same four operations with constant operands (sqrt(-42.0), 1.0 / 0.0, DBL_MAX * 2.0, 0.1 + 0.2) and no pragma, the way most programs are written. A compiler that works an answer out while compiling raises its flags inside the compiler, and the program never sees them. Each machine was asked three times: as written, with #pragma STDC FENV_ACCESS ON added at the top, and with the pragma at -O2.

Real output — GCC 13.3, ubuntu:24.04 x86-64 in Docker (GCC 14.4 in Debian's gcc:14 image raises the same flags)
$ cc -std=c17 -Wall -Wextra -pedantic folded_flags.c -lm -o folded_flags
$ ./folded_flags
sqrt(-42.0)   raised: FE_INVALID
1.0 / 0.0     raised: FE_DIVBYZERO
DBL_MAX * 2.0 raised: FE_OVERFLOW FE_INEXACT
0.1 + 0.2     raised: (none)
$ { echo '#pragma STDC FENV_ACCESS ON'; cat folded_flags.c; } > pragma_on.c
$ cc -std=c17 -Wall -Wextra -pedantic pragma_on.c -lm -o pragma_on
pragma_on.c:1: warning: ignoring '#pragma STDC FENV_ACCESS' [-Wunknown-pragmas]
    1 | #pragma STDC FENV_ACCESS ON
      | 
$ ./pragma_on
sqrt(-42.0)   raised: FE_INVALID
1.0 / 0.0     raised: FE_DIVBYZERO
DBL_MAX * 2.0 raised: FE_OVERFLOW FE_INEXACT
0.1 + 0.2     raised: (none)
$ cc -std=c17 -Wall -Wextra -pedantic -O2 pragma_on.c -lm -o pragma_on
pragma_on.c:1: warning: ignoring '#pragma STDC FENV_ACCESS' [-Wunknown-pragmas]
    1 | #pragma STDC FENV_ACCESS ON
      | 
$ ./pragma_on
sqrt(-42.0)   raised: FE_INVALID
1.0 / 0.0     raised: (none)
DBL_MAX * 2.0 raised: (none)
0.1 + 0.2     raised: (none)
Real output — Apple clang 21, x86-64 macOS 26
$ cc -std=c17 -Wall -Wextra -pedantic folded_flags.c -lm -o folded_flags
$ ./folded_flags
sqrt(-42.0)   raised: FE_INVALID
1.0 / 0.0     raised: (none)
DBL_MAX * 2.0 raised: (none)
0.1 + 0.2     raised: (none)
$ { echo '#pragma STDC FENV_ACCESS ON'; cat folded_flags.c; } > pragma_on.c
$ cc -std=c17 -Wall -Wextra -pedantic pragma_on.c -lm -o pragma_on
$ ./pragma_on
sqrt(-42.0)   raised: FE_INVALID
1.0 / 0.0     raised: FE_DIVBYZERO
DBL_MAX * 2.0 raised: FE_OVERFLOW FE_INEXACT
0.1 + 0.2     raised: FE_INEXACT
$ cc -std=c17 -Wall -Wextra -pedantic -O2 pragma_on.c -lm -o pragma_on
$ ./pragma_on
sqrt(-42.0)   raised: FE_INVALID
1.0 / 0.0     raised: FE_DIVBYZERO
DBL_MAX * 2.0 raised: FE_OVERFLOW FE_INEXACT
0.1 + 0.2     raised: FE_INEXACT

Unoptimised and without the pragma, GCC's program sees three of the four flags and clang's sees one. Clang computed three of the operations itself and left only the sqrt call for run time. GCC left 1.0 / 0.0 and DBL_MAX * 2.0 for run time but computed 0.1 + 0.2 itself. With the pragma, clang leaves every operation that raises a flag to run time, at -O2 as well, and all four flags reach the program. GCC warns that it is ignoring the pragma, and at -O2 it computes everything except the sqrt call. None of these runs is wrong. Each compiler is allowed its choice, and that is why the lesson's example uses volatile.

errno, a flag, or both

C gives a math function two ways to report a domain error: set errno to EDOM, or raise FE_INVALID. A library may use either or both, and C17 7.12 leaves the choice to the library, which reports it in math_errhandling. demo/errno_or_flag.c asks:

Real output — GCC 13.3 and glibc 2.39, ubuntu:24.04 x86-64 in Docker (glibc 2.41 in gcc:14 prints the same)
$ cc -std=c17 -Wall -Wextra -pedantic errno_or_flag.c -lm -o errno_or_flag
$ ./errno_or_flag
math_errhandling & MATH_ERRNO     -> 1
math_errhandling & MATH_ERREXCEPT -> 1
sqrt(-42.0): isnan 1, FE_INVALID 1, errno == EDOM 1
Real output — Apple clang 21 and Apple's libm, x86-64 macOS 26
$ cc -std=c17 -Wall -Wextra -pedantic errno_or_flag.c -lm -o errno_or_flag
$ ./errno_or_flag
math_errhandling & MATH_ERRNO     -> 0
math_errhandling & MATH_ERREXCEPT -> 1
sqrt(-42.0): isnan 1, FE_INVALID 1, errno == EDOM 0

glibc does both. Apple's library only raises the flag and leaves errno alone. A program that checks errno after sqrt catches the error on Linux and misses it on a Mac, while one that checks FE_INVALID catches it on both. On macOS, math_errhandling is a call into the library, __math_errhandling(), rather than a constant in the header, and the arm64 runner has not been asked.

A trap is something you ask for

To get the CPU exception the footnote describes, a program has to unmask one. glibc has a function for it, feenableexcept, a GNU extension outside standard C. demo/trap_invalid.c unmasks FE_INVALID and then takes the square root of -42.0:

Real output — GCC 13.3 and glibc 2.39, ubuntu:24.04 x86-64 in Docker; bash's own Floating point exception line left out
$ cc -std=c17 -Wall -Wextra -pedantic trap_invalid.c -lm -o trap_invalid
$ ./trap_invalid; echo "exit $?"
FE_INVALID now traps; calling sqrt(-42.0)
exit 136

Now the invalid operation stops the program. Exit status 136 is 128 plus 8, and signal 8 is SIGFPE. The flag became a trap because the program asked for one.

Real output — Apple clang 21, x86-64 macOS 26
$ cc -std=c17 -Wall -Wextra -pedantic trap_invalid.c -lm -o trap_invalid
trap_invalid.c:13:5: error: call to undeclared function 'feenableexcept'; ISO C99 and later do not support implicit function declarations [-Wimplicit-function-declaration]
   13 |     feenableexcept(FE_INVALID);
      |     ^
trap_invalid.c:13:5: note: did you mean 'feraiseexcept'?
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/fenv.h:325:12: note: 'feraiseexcept' declared here
  325 | extern int feraiseexcept(int /* excepts */);
      |            ^
1 error generated.

Apple's <fenv.h> declares no feenableexcept for any architecture, so the same program does not build on a Mac. The header does name trap-enable bits in the arm64 control register (__fpcr_trap_invalid and its siblings, set through fesetenv). This library has not run that on the arm64 runner, so this page makes no claim about what it does.

The CPU exception people usually mean

What does stop a program on x86-64 without being asked is integer division by zero. An int has no NaN or infinity to return, and C17 6.5.5 makes the division undefined behaviour. On x86-64 the divide instruction faults, and the kernel delivers the same SIGFPE, named for floating point but raised here by integer arithmetic. demo/integer_divide.c divides 1 by zero and then 7 by zero:

Real output — GCC 13.3, ubuntu:24.04 x86-64 in Docker; bash's own Floating point exception lines left out
$ cc -std=c17 -Wall -Wextra -pedantic integer_divide.c -o integer_divide
$ ./integer_divide; echo "exit $?"
1 / 0 = 0
exit 136
$ cc -std=c17 -Wall -Wextra -pedantic -O2 integer_divide.c -o integer_divide
$ ./integer_divide; echo "exit $?"
1 / 0 = 0
exit 136
Real output — GCC 14.4, Debian's gcc:14 image, x86-64 in Docker; bash's own Floating point exception lines left out
$ cc -std=c17 -Wall -Wextra -pedantic integer_divide.c -o integer_divide
$ ./integer_divide; echo "exit $?"
exit 136
$ cc -std=c17 -Wall -Wextra -pedantic -O2 integer_divide.c -o integer_divide
$ ./integer_divide; echo "exit $?"
1 / 0 = 0
exit 136
Real output — Apple clang 21, x86-64 macOS 26; the shell's own Floating point exception lines left out
$ cc -std=c17 -Wall -Wextra -pedantic integer_divide.c -o integer_divide
$ ./integer_divide; echo "exit $?"
exit 136
$ cc -std=c17 -Wall -Wextra -pedantic -O2 integer_divide.c -o integer_divide
$ ./integer_divide; echo "exit $?"
1 / 0 = 0
exit 136

Every run ends in SIGFPE; what differs is which line. 1 / 0 prints 0 under GCC 13 even unoptimised, and under GCC 14 and clang only at -O2, and wherever 1 / 0 did not trap, 7 / 0 did. The quotient of 1 by an int is 0 for every divisor except 1 and -1, so a compiler may replace that division with a comparison. GCC 13's unoptimised code for 1 / zero is a cmp and a cmove with no divide instruction, and GCC 14's is an idiv. A division by zero is undefined behaviour, so both outcomes are allowed. That is why no answer key in this library records one. All three runs were on x86-64; what the arm64 runner does was not measured.

Comparing floats

The book section's other half is ==. float_equality_c.c compares the sums and prints their bits, copied with memcpy into an unsigned integer of the same size:

float_equality_c.c in full — pasted here by tools/run_examples.py from the file CI runs.

/* == on floating-point numbers compares the values actually stored, and 0.1,
   0.2 and 0.3 are not stored exactly. In float the rounding happens to land
   0.1f + 0.2f on the same bits as 0.3f; in double it lands one step away.
   memcpy into a same-sized unsigned integer shows the bits without breaking
   the aliasing rules. */
#include <float.h>
#include <inttypes.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

_Static_assert(sizeof(float) == sizeof(uint32_t), "float is 32 bits");
_Static_assert(sizeof(double) == sizeof(uint64_t), "double is 64 bits");

static uint32_t bits32(float f)
{
    uint32_t u;
    memcpy(&u, &f, sizeof u);
    return u;
}

static uint64_t bits64(double d)
{
    uint64_t u;
    memcpy(&u, &d, sizeof u);
    return u;
}

int main(void)
{
    float sum_f = 0.1f + 0.2f;
    printf("0.1f + 0.2f == 0.3f  -> %d\n", sum_f == 0.3f);
    printf("  0.1f + 0.2f bits   =  %08" PRIx32 "\n", bits32(sum_f));
    printf("  0.3f bits          =  %08" PRIx32 "\n", bits32(0.3f));

    double sum_d = 0.1 + 0.2;
    printf("0.1 + 0.2 == 0.3     -> %d\n", sum_d == 0.3);
    printf("  0.1 + 0.2 bits     =  %016" PRIx64 "\n", bits64(sum_d));
    printf("  0.3 bits           =  %016" PRIx64 "\n", bits64(0.3));
    printf("  0.1 + 0.2          =  %.17g\n", sum_d);

    /* An epsilon test. DBL_EPSILON is the gap between 1.0 and the next double,
       so this is a tolerance sized for numbers near 1. */
    printf("fabs(0.1 + 0.2 - 0.3) = %.17g\n", fabs(sum_d - 0.3));
    printf("DBL_EPSILON           = %.17g\n", DBL_EPSILON);
    printf("FLT_EPSILON           = %.9g\n", FLT_EPSILON);
    printf("fabs(0.1 + 0.2 - 0.3) <= DBL_EPSILON -> %d\n", fabs(sum_d - 0.3) <= DBL_EPSILON);

    /* NaN is the one value not equal to itself, so ask isnan instead. */
    double nan = NAN, inf = INFINITY;
    printf("NAN == NAN           -> %d\n", nan == nan);
    printf("NAN != NAN           -> %d\n", nan != nan);
    printf("isnan(NAN)           -> %d\n", isnan(nan) != 0);
    printf("isfinite(INFINITY)   -> %d\n", isfinite(inf) != 0);
    printf("isfinite(DBL_MAX)    -> %d\n", isfinite(DBL_MAX) != 0);
    printf("isnan(INFINITY - INFINITY) -> %d\n", isnan(inf - inf) != 0);
    return 0;
}

Verified output of float_equality_c.c — regenerated by tools/run_examples.py, never hand-typed.

0.1f + 0.2f == 0.3f  -> 1
  0.1f + 0.2f bits   =  3e99999a
  0.3f bits          =  3e99999a
0.1 + 0.2 == 0.3     -> 0
  0.1 + 0.2 bits     =  3fd3333333333334
  0.3 bits           =  3fd3333333333333
  0.1 + 0.2          =  0.30000000000000004
fabs(0.1 + 0.2 - 0.3) = 5.5511151231257827e-17
DBL_EPSILON           = 2.2204460492503131e-16
FLT_EPSILON           = 1.1920929e-07
fabs(0.1 + 0.2 - 0.3) <= DBL_EPSILON -> 1
NAN == NAN           -> 0
NAN != NAN           -> 1
isnan(NAN)           -> 1
isfinite(INFINITY)   -> 0
isfinite(DBL_MAX)    -> 1
isnan(INFINITY - INFINITY) -> 1

In float, 0.1f + 0.2f rounds to exactly the bits of 0.3f, 3e99999a, so == is true. In double the sum is 3fd3333333333334 and 0.3 is 3fd3333333333333: they are neighbours, one step apart, and == is false. These are the same bits Rust prints for f32 and f64, because both languages store IEEE 754 binary32 and binary64. Neither language chose the rounding; the format did.

DBL_EPSILON is the gap between 1.0 and the next double, 2.2204460492503131e-16. The sum is off by 5.5511151231257827e-17, a quarter of that, so the epsilon test passes. It is a tolerance sized for numbers near 1. Far from 1 the gap between neighbouring doubles is a different size, and a tolerance has to scale with the numbers being compared. NaN is the one value that is not equal to itself: NAN == NAN is 0 and NAN != NAN is 1, so isnan is the only reliable test, and isfinite rules out both NaN and the infinities in one call.

If you are coming from another language

Rust. The same defaults: (-42.0_f64).sqrt() is NaN and 1.0 / 0.0 is infinity, with no trap. The standard library has no stable way to read the status flags, so the information that C's fetestexcept returns is not available. Integer division is where Rust differs from C: dividing by a zero that is only known at run time panics with attempt to divide by zero (exit status 101, rustc 1.98), because Rust checks the divisor before the CPU sees the division. The Rust library takes the book's section claim by claim in Comparing numbers, claims checked ↗, and shows the bits in What a float stores ↗.

Python. Python turns two of these exceptions into real ones: math.sqrt(-42.0) raises ValueError and 1.0 / 0.0 raises ZeroDivisionError, where C returns NaN and infinity. It does not do so consistently: 1e308 * 10 is inf and float('inf') - float('inf') is nan, with no exception (CPython 3.14). math.isclose is the scaled tolerance test. See Float equality and NaN ↗.

ABAP. (Not machine-checked — CI cannot run ABAP.) ABAP takes Python's route rather than C's for its binary floating-point type f: a division by zero raises CX_SY_ZERODIVIDE, a result too large for the type raises CX_SY_ARITHMETIC_OVERFLOW, and a function argument outside its domain, such as sqrt of a negative number, raises CX_SY_ARG_OUT_OF_DOMAIN. These are catchable class-based exceptions, so an ABAP program meets the error at the statement that caused it, not in a flag afterwards.

See also