Editorial: Today's pick is a reminder that some foundational C library behaviors still vary enough to wreck production code. If you parse floating numbers in C and expect a single, portable rule for detecting errors, you'll want to read this.
In Brief
No portable way to detect string-to-float conversion errors in standard C
Why this matters now: Developers writing cross-platform C that parses numeric input risk silently misreporting overflow, underflow, or invalid values because string-to-float conversion semantics differ between C standard, POSIX, glibc, and musl.
A recent write-up walks through the messy reality of strtod/strtof/strtold and their platform-specific behaviors, showing that the usual tricks aren't universally reliable. According to the original post, common advice — like setting errno to 0, calling strtod, and then checking result and errno — only holds consistently on POSIX-like systems and when you know how a particular libc reports FP errors. The piece explains why underflow detection often degenerates into ad-hoc string checks and why relying on floating-point exceptions (fenv) requires compiler support and care.
"if you're targeting posix, set errno to 0 before the call, and afterward check result == 0.0 && errno == ERANGE," the post notes — which reads less like guidance and more like a platform-specific hack that you must remember per target.
Deep Dive
No portable way to detect string-to-float conversion errors in standard C
Why this matters now: Projects that parse numeric input in security-sensitive, financial, or scientific codebases need robust error detection; incorrect or silent conversions can lead to wrong decisions and data corruption across different OSes and libcs.
The heart of the problem is that the C standard leaves some behavior around strtod-style parsing intentionally implementation-defined: how overflows, underflows, invalid syntax, and exceptional conditions are reported is not unified. POSIX codifies one approach — use errno and special overflow values like HUGE_VAL — but not every libc or platform adheres to the same observable signals. The write-up shows practical differences between glibc and musl, and highlights that code which checks errno+HUGE_VAL on Linux with glibc may misbehave elsewhere.
Floating-point exceptions and the fenv API are another angle. You can clear the FP exception flags with feclearexcept(), do the parse, and then test them with fetestexcept() to see whether an overflow or underflow occurred. But that's not a drop-in solution: many compilers optimize under assumptions that let them elide reads of the FP environment unless you explicitly enable it. The standard has a pragma — #pragma STDC FENV_ACCESS ON — to promise the compiler you will access the environment, but support for that pragma varies, and relying on it couples your code to subtle compiler and optimization behavior.
Underflow is particularly ugly because of denormals and subnormal results. A parser may silently return a subnormal value (very small but nonzero) without setting errno, or it may flush-to-zero depending on platform flags, rounding modes, or the libc implementation. To robustly detect when a string represents a value so small that it should be considered "underflow" you often end up inspecting the input string (look for exponent ranges outside a safe band) or using a higher-precision parse to decide if the value is representable — neither of which are attractive if you just wanted a simple, portable strtod.
Practical recommendations from the write-up and community reactions boil down to three choices:
- Constrain your deployment: If you control the runtime (POSIX + glibc, or a constrained set of platforms), use the documented pattern for those targets: clear errno, call strtod, check errno and compare against HUGE_VAL/HUGE_VALF/TYPE-specific sentinels.
- Use a dedicated numeric parser: Libraries like fast_float and other small, portable decimal-to-binary parsers are designed to be deterministic across platforms and avoid relying on libc error signaling. They typically return explicit status codes you can check.
- Lift parsing to a higher-level language or runtime that guarantees semantics: If your application has a small, trusted parsing layer in a language with well-specified parsing (Rust, Go, Java), perform parsing there and then pass validated binary values to C code.
Nothing here is news to long-time systems programmers, but the write-up is a useful, concrete reminder: portability isn't just about API names, it's about observable error channels (errno, FP flags, return sentinels) and compiler/OS behavior. The Hacker News thread echoed that sentiment — developers called this a "portability footgun" and explained why teams often centralize parsing, add regression tests across target platforms, or avoid stdlib parsers in critical code.
If you're deciding what to do right now, favor deterministic libraries that return explicit parse results over relying on errno or floating-point state unless you can enforce the exact combination of libc, compiler flags, and platform behavior in every deployment. And if you're maintaining a library that exposes C parsing semantics, document exactly which platforms you support and what callers should do to detect and handle parse errors.
"there's also a standard pragma for this purpose: #pragma STDC FENV_ACCESS ON," the post cautions — useful when supported, fragile when not.
Closing Thought
C remains a language of trade-offs: its stdlib gives you portable APIs, but not always portable observability. When input correctness matters, treat string-to-float parsing as a subsystem with its own tests, chosen dependency, and explicit error contract — and consider moving parsing out of places where subtle differences can quietly break higher-level logic.