Format nested exceptions in std::exception formatter (#4844)

Detect when a std::exception also derives from std::nested_exception
(e.g. via std::throw_with_nested) and unwind the whole chain, joining
each level with ": ". The type-name flag ({:t}) is honored at every
level, and the std::exception_ptr formatter inherits this behavior.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Avi Kivity 2026-07-12 18:50:10 +03:00 committed by GitHub
parent 7077b016cc
commit 1a99c2630c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 65 additions and 2 deletions

View File

@ -637,7 +637,12 @@ struct formatter<
template <typename Context>
auto format(const std::exception& ex, Context& ctx) const
-> decltype(ctx.out()) {
auto out = ctx.out();
return write(ctx.out(), ex);
}
private:
template <typename OutputIt>
auto write(OutputIt out, const std::exception& ex) const -> OutputIt {
#if FMT_USE_RTTI
if (with_typename_) {
out = detail::write_demangled_name(out, typeid(ex));
@ -645,7 +650,24 @@ struct formatter<
*out++ = ' ';
}
#endif
return detail::write_bytes<char>(out, string_view(ex.what()));
out = detail::write_bytes<char>(out, string_view(ex.what()));
#if FMT_USE_RTTI
// If the exception carries a nested exception (e.g. via
// std::throw_with_nested), format the whole chain.
if (auto* nested = dynamic_cast<const std::nested_exception*>(&ex)) {
if (auto ep = nested->nested_ptr()) {
out = detail::write(out, string_view(": "));
try {
std::rethrow_exception(ep);
} catch (const std::exception& nested_ex) {
out = write(out, nested_ex);
} catch (...) {
out = detail::write(out, string_view("unknown exception"));
}
}
}
#endif
return out;
}
};

View File

@ -402,6 +402,34 @@ TEST(std_test, exception) {
StartsWith("std::filesystem::filesystem_error: "));
}
#endif
#if FMT_USE_RTTI
// Nested exceptions (e.g. from std::throw_with_nested) are unwound.
try {
try {
throw std::runtime_error("inner");
} catch (...) {
std::throw_with_nested(std::runtime_error("outer"));
}
} catch (const std::exception& ex) {
EXPECT_EQ("outer: inner", fmt::format("{}", ex));
}
// Multiple levels of nesting.
try {
try {
try {
throw std::runtime_error("level 3");
} catch (...) {
std::throw_with_nested(std::runtime_error("level 2"));
}
} catch (...) {
std::throw_with_nested(std::runtime_error("level 1"));
}
} catch (const std::exception& ex) {
EXPECT_EQ("level 1: level 2: level 3", fmt::format("{}", ex));
}
#endif
}
TEST(std_test, exception_ptr) {
@ -421,6 +449,19 @@ TEST(std_test, exception_ptr) {
#if FMT_USE_RTTI
EXPECT_EQ(fmt::format("{:t}", p2),
"my_ns1::my_ns2::my_exception: My Exception");
// Nested exceptions are unwound through an exception_ptr too.
std::exception_ptr p3;
try {
try {
throw std::runtime_error("inner");
} catch (...) {
std::throw_with_nested(std::runtime_error("outer"));
}
} catch (...) {
p3 = std::current_exception();
}
EXPECT_EQ(fmt::format("{}", p3), "outer: inner");
#endif
}