From 1a99c2630cdf07f4e18924adc5e10650aaf67bc6 Mon Sep 17 00:00:00 2001 From: Avi Kivity Date: Sun, 12 Jul 2026 18:50:10 +0300 Subject: [PATCH] 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) --- include/fmt/std.h | 26 ++++++++++++++++++++++++-- test/std-test.cc | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/include/fmt/std.h b/include/fmt/std.h index 207c9d35..18b12abc 100644 --- a/include/fmt/std.h +++ b/include/fmt/std.h @@ -637,7 +637,12 @@ struct formatter< template auto format(const std::exception& ex, Context& ctx) const -> decltype(ctx.out()) { - auto out = ctx.out(); + return write(ctx.out(), ex); + } + + private: + template + 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(out, string_view(ex.what())); + out = detail::write_bytes(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(&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; } }; diff --git a/test/std-test.cc b/test/std-test.cc index d64ef956..8fb92f79 100644 --- a/test/std-test.cc +++ b/test/std-test.cc @@ -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 }