Standard C has no portable way to detect strtod errors, a blog post argues

The post is a sequel to the author's earlier piece on math_errhandling, the macro that tells you which error-handling mechanisms the math.h functions support: errno (MATH_ERRNO), floating-point exceptions (MATH_ERREXCEPT), or both. The new point is that one family of functions is affected by math_errhandling even though it does not live in math.h: the string-to-float converters strtod, strtof, strtold, strtod32, strtod64 and strtod128. The author notes that the Linux man pages for these functions do not mention this at all.

What the C standard says, as the author describes it: if math_errhandling does not advertise errno support (as on musl), the conversion functions do not set errno on error. If the result underflows, the function is not required to report an error at all. The return value alone cannot tell you that overflow happened, so you must use one of the two mechanisms. The author's attempt at a standards-blessed portable check clears FE_OVERFLOW and FE_UNDERFLOW, sets errno to 0, calls strtod, then tests errno == ERANGE if math_errhandling & MATH_ERRNO is set, and fetestexcept(FE_OVERFLOW | FE_UNDERFLOW) otherwise. Even that does not guarantee underflow is detected, because reporting underflow is optional for the implementation.

POSIX specifies the functions differently. On overflow it requires HUGE_VAL, HUGE_VALF or HUGE_VALL (with the sign of the value) and errno set to ERANGE; on underflow it requires a value no greater in magnitude than the smallest normalized positive number, and errno set to ERANGE. The author's reading is that POSIX does not care about math_errhandling and always requires errno to be set. He or she finds it notable that neither the strtod(3) man page nor the POSIX strtod(3p) page marks this as an extension of standard C. Whether POSIX's behavior is even compatible with standard C is, in the author's words, unclear: setting errno regardless of math_errhandling is permitted for math.h functions, but the errno.h wording and the functions' own descriptions make no mention of setting errno when math_errhandling & MATH_ERRNO is zero, which suggests POSIX's behavior is non-conformant.

Malformed input is a separate matter. For a string that cannot be parsed as a number, the C standard specifies no error at all; you are supposed to pass endptr and check afterward whether endptr == nptr, meaning no data was parsed. POSIX says that if no conversion could be performed, 0 is returned and errno may be set to EINVAL, which the author reads as implementation-defined. That matters because a common idiom is to set errno to 0, call strtod, and test errno != 0, and the strtod(3) man page recommends exactly this. The author shows it is not portable even among POSIX libcs: calling strtod on the string x with a null endptr and then printing errno prints 0 on glibc, because glibc's strtod never sets EINVAL, and prints 22 on musl, which does set errno to EINVAL. The Linux man page does not document this difference.

The author also reads musl's behavior as not conformant to standard C, since it sets errno to a nonzero value in a function that has other documented error conditions, and invalid input is not an error condition as far as standard C is concerned. Musl could argue that because it does not set errno in its math.h functions, the errno condition in strtod's description no longer applies, but the author calls that clearly a stretch. The conclusion: the standard needs clearer wording here.

The post closes with a list of the correct ways to check for errors, offered just for fun to hammer the point home. Overflow, targeting POSIX: set errno to 0 before the call and afterward check copysign(result, 1.0) == HUGE_VAL && errno == ERANGE. Otherwise: set errno to 0 and call feclearexcept(FE_OVERFLOW) before, and if copysign(result, 1.0) == HUGE_VAL, check either errno == ERANGE or fetestexcept(FE_OVERFLOW) depending on math_errhandling. If you target a single implementation whose mechanism you know, you can skip the math_errhandling check. When using feclearexcept and fetestexcept, tell the compiler you may touch the floating-point environment: on gcc the flag is -ftrapping-math, which is the default unless you use -ffast-math, and the standard pragma STDC FENV_ACCESS ON is not supported by gcc.

Underflow, targeting POSIX: set errno to 0 and afterward check result == 0.0 && errno == ERANGE, making sure errno is specifically ERANGE and not merely nonzero. Otherwise, for a single implementation, check whether it documents its underflow behavior for math.h functions. That behavior is implementation-defined, so it is technically required to be documented, but the author says even clang does not bother in practice. If it is documented and underflow is reported, use errno or the floating-point exception flags according to that implementation's math_errhandling. If not, the author is out of options except one: when result == 0.0, check yourself whether the input string has any nonzero digits before the exponent, and if so underflow occurred. This is difficult to get right, and the author advises reading the standard's description of strtod and the related functions to cover all edge cases.

Invalid input (no conversion): pass &endptr as the second argument and check endptr == nptr. errno cannot be relied on.

Key facts

  • ISO C ties errno use in strtod, strtof, strtold, strtod32, strtod64 and strtod128 to math_errhandling, and underflow need not be reported at all.
  • POSIX always requires errno to be set to ERANGE on overflow or underflow, which the author argues may be non-conformant to standard C.
  • For unparseable input, calling strtod on the string x prints errno 0 on glibc and 22 (EINVAL) on musl.
  • The only portable check for no conversion is passing endptr and testing endptr == nptr; the errno != 0 idiom from the strtod(3) man page is not portable.
  • The author concludes the standard needs clearer wording.

Why it matters

The idiom of zeroing errno, calling strtod and testing errno afterward is what the strtod(3) man page itself suggests, and it is widely used. The post shows that it gives different answers for the same bad input on glibc and musl, and that the C standard itself defines no error for malformed input. The gap between ISO C, POSIX and what libraries actually do is the substance of the piece, and the author's own conclusion is that the standard needs clearer wording.

Who it affects

C programmers who parse numbers from text and want error handling that works across libraries, and people who write or maintain libc implementations such as glibc and musl. Code that relies on errno after strtod to catch invalid input will behave differently depending on which of the two libraries it runs against.

How to use it

For invalid input, pass a non-null endptr and check endptr == nptr; do not rely on errno. For overflow on POSIX targets, set errno to 0 before the call and check that the result equals HUGE_VAL (via copysign) and errno == ERANGE. On non-POSIX targets, branch on math_errhandling between errno and the FE_OVERFLOW flag. Underflow cannot be reliably detected in portable code; the author's last-resort option is to test whether the input has nonzero digits before the exponent when the result is 0.0. On gcc, the pragma STDC FENV_ACCESS ON is not supported, and -ftrapping-math is the default unless -ffast-math is used.

How solid is it

This is a single personal blog post, a first-person analysis in which the author quotes the standard, POSIX and the man pages and gives a short code example with its output. Several of the conformance judgments are explicitly the author's own reading (for example, that musl's EINVAL is not conformant to standard C). Only glibc and musl are compared, and no library version numbers are given for the behavior described.

Risks and caveats

The conformance conclusions are one author's interpretation of ambiguous standard text, not a ruling by any standards body. No bug report, standards-committee proposal or defect report is mentioned, and no reaction from glibc, musl or standards maintainers is reported. Even the author's portable overflow and underflow check cannot guarantee underflow detection, since reporting it is optional, and the manual workaround for underflow is described as difficult to get right.

“it's clear to me that the standard needs clearer wording here.”

— the blog post's author