This page is a snapshot from the LWG issues list, see the Library Active Issues List for more information and the meaning of New status.

4502. std::execution::when_all Is Never Nothrow Connectable

Section: 33.9.12.12 [exec.when.all] Status: New Submitter: Robert Leahy Opened: 2026-01-11 Last modified: 2026-01-11

Priority: Not Prioritized

View all other issues in [exec.when.all].

View all issues with New status.

Discussion:

The behavior of algorithms in the standard is described in terms of exposition-only machinery 33.9.2 [exec.snd.expos]. Particularly the behavior of std::execution::connect 33.9.10 [exec.connect] for such algorithms is described in terms of basic-sender::connect which is conditionally noexcept:

The expression in the noexcept clause of the connect member function of basic-sender is:

is_nothrow_constructible_v<basic-operation<Self, Rcvr>, Self, Rcvr>

The constructor of basic-operation is also conditionally noexcept:

The expression in the noexcept clause of the constructor of basic-operation is:

is_nothrow_constructible_v<basic-state<Self, Rcvr>, Self, Rcvr> &&
noexcept(connect-all(this, std::forward<Sndr>(sndr), indices-for<Sndr>()))

This cascade of chasing conditional noexcept doesn't end there, instead moving to basic-state:

The expression in the noexcept clause of the constructor of basic-state is

is_nothrow_move_constructible_v<Rcvr> &&
nothrow-callable>decltype(impls-for<tag_of_t<Sndr>>::get-state), Sndr, Rcvr&> &&
(same_as<state-type<Sndr, Rcvr>, get-state-result> ||
 is_nothrow_constructible_v<state-type<Sndr, Rcvr>, get-state-result>)

where get-state-result is

call-result-t<decltype(impls-for<tag_of_t<Sndr>>::get-state), Sndr, Rcvr&>.

This finally implicates something in 33.9.12.12 [exec.when.all] as we must now look to the noexcept clause of its get-state implementation:

The member impls-for<when_all_t>::get-state is initialized with a callable object equivalent to the following lambda expression:

[]<class Sndr, class Rcvr>(Sndr&& sndr, Rcvr& rcvr) noexcept(noexcept(e)) -> decltype(e) {
  return e;
}

where e is the expression

std::forward<Sndr>(sndr).apply(make-state<Rcvr>())

and where make-state is the following exposition-only class template:

enum class disposition { started, error, stopped };             // exposition only

template<class Rcvr>
struct make-state {
  template<class... Sndrs>
  auto operator()(auto, auto, Sndrs&&... sndrs) const {
    using values_tuple = see below;
    using errors_variant = see below;
    using stop_callback = stop_callback_for_t<stop_token_of_t<env_of_t<Rcvr>>, on-stop-request>;

    struct state-type {
      void arrive(Rcvr& rcvr) noexcept {                        // exposition only
        if (0 == --count) {
          complete(rcvr);
        }
      }

      void complete(Rcvr& rcvr) noexcept;                       // exposition only

      atomic<size_t> count{sizeof...(sndrs)};                   // exposition only
      inplace_stop_source stop_src{};                           // exposition only
      atomic<disposition> disp{disposition::started};           // exposition only
      errors_variant errors{};                                  // exposition only
      values_tuple values{};                                    // exposition only
      optional<stop_callback> on_stop{nullopt};                 // exposition only
    };

    return state-type{};
  }
};

Notice that the overloaded function call operator does not have a noexcept clause meaning connecting a std::execution::when_all sender is never noexcept.

This is a library wording issue because if the design intent was for the above-described operation to be unconditionally noexcept(false) there would be no reason for get-state to feature noexcept(noexcept(e)) (since that expression would've been intended to be a contradiction).

Moreover in returning a default-initialized state-type the above-discussed function call operator performs the following operations:

Since none of these throw the overloaded function call operator can be made noexcept(true).

Proposed resolution:

Update 33.9.12.12 [exec.when.all] as follows:

[...]

and where make-state is the following exposition-only class template:

enum class disposition { started, error, stopped };             // exposition only

template<class Rcvr>
struct make-state {
  template<class... Sndrs>
  auto operator()(auto, auto, Sndrs&&... sndrs) const noexcept {
    using values_tuple = see below;
    using errors_variant = see below;
    using stop_callback = stop_callback_for_t<stop_token_of_t<env_of_t<Rcvr>>, on-stop-request>;

    struct state-type {
      void arrive(Rcvr& rcvr) noexcept {                        // exposition only
        if (0 == --count) {
          complete(rcvr);
        }
      }

      void complete(Rcvr& rcvr) noexcept;                       // exposition only

      atomic<size_t> count{sizeof...(sndrs)};                   // exposition only
      inplace_stop_source stop_src{};                           // exposition only
      atomic<disposition> disp{disposition::started};           // exposition only
      errors_variant errors{};                                  // exposition only
      values_tuple values{};                                    // exposition only
      optional<stop_callback> on_stop{nullopt};                 // exposition only
    };

    return state-type{};
  }
};

[...]