Document Number: p2927r1
Date: 2024-02-14
Target: LEWG
Revises: p2927r0
Reply to: Arthur O'Dwyer (, Gor Nishanov (

Inspecting exception_ptr


Provide facility to observe exceptions stored in std::exception_ptr without throwing or catching exceptions.


This is a followup to two previous papers in this area:

Date Link Title
Feb 7, 2018 Runtime introspection of exception_ptr
Oct 6, 2018 How to catch an exception_ptr without even try-ing

These papers received positive feedback. In 2018 Rapperswil meeting, EWG expressed strong desire in having such facility. This was reaffirmed in 2023 Kona meeting.

This paper brings back exception_ptr inspection facility in a simplified form addressing the earlier feedback.

Proposal at a glance

We introduce a single function try_cast that takes std::exception_ptr as an argument e and returns a pointer to an object referred to by e.

template <typename T>
const T* 
try_cast(const exception_ptr& e) noexcept;


Given the following error classes:

struct Foo {
    virtual ~Foo() {}
    int i = 1;
struct Bar : Foo, std::logic_error {
    Bar() : std::logic_error("This is Bar exception") {}
    int j = 2;
struct Baz : Bar {};

The execution of the following program

int main() {
    const auto exp = std::make_exception_ptr(Baz());
    if (auto* x = std::try_cast<Baz>(exp))
        printf("got '%s' i: %d j: %d\n", typeid(*x).name(), x->i, x->j); 
    if (auto* x = std::try_cast<Bar>(exp))
        printf("got '%s' i: %d j: %d\n", typeid(*x).name(), x->i, x->j);
    if (auto* x = std::try_cast<Foo>(exp))
        printf("got '%s' i: %d\n", typeid(*x).name(), x->i);

results in this output:

got '3Baz' what:'This is Bar exception' i: 1 j: 2
got '3Baz' what:'This is Bar exception' i: 1 j: 2
got '3Baz' i: 1

See implementation for GCC and MSVC using available (but undocumented) APIs

Simplification post Kona 2023

Previous revision tentatively proposed a complicated signature imitating the syntax of a catch-parameter, as in old::try_cast<const std::exception&>(p). In Kona, we were convinced to simplify the signature to assume catch-by-const-reference no matter what: std::try_cast<std::exception>(p).

P2927R0 proposed that try_cast should be able to catch pointer types, just like an ordinary catch clause. That is, not only were you allowed to inspect a thrown Derived object with old::try_cast<const Base&> (which would return a possibly null const Base*), you were also allowed to inspect a thrown Derived* object with old::try_cast<const Base*> (which would return a possibly null const Base**). This turned out to be unimplementable. When a catch-handler catches Derived* as Base*, it may need to adjust the pointer for multiple and/or virtual base classes. The pointer caught by the core language, then, is a temporary. We can't return a const Base** pointing to that temporary adjusted pointer, because there's nowhere for the temporary adjusted pointer to live after the call to try_cast has returned.

In other words, the new design has a strict invariant: the pointer returned from try_cast always points to the in-flight exception object itself. It never points to any other object, such as a temporary or global. Thus, we must disallow:

    using IntPtr = int*;
    std::nullptr_t np;
    auto p = std::make_exception_ptr(np);
      // The in-flight exception object is of type std::nullptr_t
    const IntPtr *ex = old::try_cast<IntPtr>(p);
      // ex cannot possibly point to the in-flight exception object, because the in-flight object is not an IntPtr!
    try {
    } catch (const IntPtr& ex) {
        // OK, ex refers to a temporary that lives only as long as this catch block

Our solution is simply to extend our Mandates element to also forbid std::try_cast with a template argument of pointer or pointer-to-member type; these are the only two kinds of types where a core-language catch-handler parameter would sometimes bind to a temporary, so these are the only kinds of types we need to forbid. Later, we found that [ScyllaDB] had independently implemented the same solution (i.e. explicitly forbid pointer types) in 2022.

Throwing pointers is rare — probably unheard of in real code. This does prevent users from using std::try_cast<const char*>(p) to inspect the results of throw "foo", which comes up sometimes in example code; but it shouldn't happen in real code.

Pattern matching

We expect that try_cast will be integrated in the pattern matching facility and will allow inspection of exception_ptr as follows:

inspect (eptr) {
   <logic_error> e => { ... }
   <exception> e => { ... }
   nullptr => { puts("no exception"); }
   __ => { puts("some other exception"); }

Other names considered but rejected


GCC, MSVC implementation is possible using available (but undocumented) APIs Implementability was also confirmed by MSVC and libstdc++ implementors.

A similar facility is available in Folly and supports Windows, libstdc++, and libc++ on linux/apple/freebsd.

Implementation there under the names: folly::exception_ptr_get_object folly::exception_ptr_get_type

Extra constraint imposed by MSVC ABI: it doesn't have information stored to do a full dynamic_cast. It can only recover types for which a catch block could potentially match. This does not conflict with the try_cast facility offered in this paper.

Arthur has implemented P2927R1 std::try_cast in his fork of libc++; see [libc++] and [Godbolt].

Usage experience

A version of exception_ptr inspection facilities is deployed widely in Meta as a part of Folly's future continuation matching.

ScyllaDB implements almost exactly the wording of this proposal, under the name try_catch<E>(p); see [ScyllaDB]. The only difference is that they return E* instead of const E*. We hear from them that they don't actually use the mutability for anything; and even if they did, they could add const_cast as mentioned above.

Proposed wording (relative to n4950)

In section [exception.syn] add definition for try_cast as follows:

exception_ptr current_exception() noexcept;
[[noreturn]] void rethrow_exception(exception_ptr p);
template <class E>
  const E* try_cast(const exception_ptr& p) noexcept;

template <class T> [[noreturn]] void throw_with_nested(T&& t);

Modify paragraph 7 of section Exception propagation [propagation] as follows:

For purposes of determining the presence of a data race, operations on exception_ptr objects shall access and modify only the exception_ptr objects themselves and not the exceptions they refer to. Use of rethrow_exception or try_cast on exception_ptr objects that refer to the same exception object shall not introduce a data race.

Add the following paragraph immediately after paragraph 8 of section Exception propagation [propagation]:

template <class E>
  const E* try_cast(const exception_ptr& p) noexcept;

Mandates: E is a cv-unqualified complete object type. E is not an array type. E is not a pointer or pointer-to-member type. [Note: When E is a pointer or pointer-to-member type, a handler of type const E& can match without binding to the exception object itself. —end note]

Returns: A pointer to the exception object referred to by p, if p is not null and a handler of type const E& would be a match [except.handle] for that exception object. Otherwise, nullptr.


Many thanks to those who provided valuable feedback, among them: Aaryaman Sagar, Barry Revzin, Gabriel Dos Reis, Jan Wilmans, Joshua Berne, Lee Howes, Lewis Baker, Michael Park, Peter Dimov, Ville Voutilainen, Yedidya Feldblum.

References (gcc and msvc implementation) Runtime introspection of exception_ptr How to catch an exception_ptr without even try-ing Pattern Matching ScyllaDb

An implementation of try cast and libc++ and matching godbolt: