Skip to main content

Updated Aug 7, 2026

Compile-time format string checking

The whole point of replacing printf is that a wrong format string shouldn't be a runtime surprise. fmt turns it into a compile error whenever the string is a literal — no printf-style silent undefined behavior, and no fmt::format_error thrown three weeks into production.

How it works today​

On C++20, fmt::format and friends take a fmt::format_string<Args...>, a type whose constructor is consteval and validates the string against the argument types at compile time. A string literal is checked with no extra syntax required.

fmt::format("{:d}", "not a number");
// error: invalid format specifier for argument of type 'const char*'
// caught before the program ever runs

FMT_STRING​

FMT_STRING(s) is the pre-C++20 macro that forces the same checking on standards where the language doesn't yet support consteval.

FMT_STRING is only needed on older standards — on C++20 the plain call is already checked
fmt::format(FMT_STRING("{}"), x); // needed pre-C++20
fmt::format("{}", x); // already checked on C++20 and later

Wrapping every literal in FMT_STRING on a C++20 codebase is harmless but unnecessary.

Opting out​

fmt::runtime(s) explicitly marks a string as not known at compile time, opting out of the check.

fmt::runtime moves the error to runtime as a thrown fmt::format_error — catch it or validate the string first
std::string user_template = load_from_config();
fmt::format(fmt::runtime(user_template), x); // any mismatch throws fmt::format_error

Once a string goes through fmt::runtime, every guarantee the compiler was giving you is gone — you're back to needing a try/catch (or upstream validation) the way you would with any other runtime input.

What is and isn't checked​

CaseCheckedWhen it fails
String literal, argument types knownYes, at compile timeCompile error
fmt::runtime("...")NoThrows fmt::format_error at the call
fmt::vformat / fmt::vformat_toNo — type-erased args bypass the checkThrows fmt::format_error at the call
Custom formatter<T> with a constexpr parseYesCompile error
Custom formatter<T> with a non-constexpr parseNoThrows fmt::format_error at the call

Making your formatter checkable​

For the compile-time check to reach a custom type's own spec grammar, that type's formatter<T>::parse must itself be constexpr. See formatter specialization for what a parse implementation looks like — most straightforward implementations are already constexpr without extra effort.

Compile-time cost​

Checking happens per call site — it is a real compile-time cost, and it is worth it

Every checked format call re-validates the string against the argument types at that specific call site. On a translation unit with hundreds of format calls, this adds up. See Header-only vs compiled mode for the broader picture of fmt's compile-time footprint and how to keep it under control.

See also​