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.

4476. run_loop should not have a set_error completion

Section: 33.12.1.1 [exec.run.loop.general] Status: New Submitter: Eric Niebler Opened: 2025-11-16 Last modified: 2026-01-16

Priority: 2

View all issues with New status.

Discussion:

When run_loop was proposed, the only implementation we had synchronized with a mutex and a condition variable. Operations on those can theoretically throw exceptions, so run_loop got a set_error_t(exception_ptr) completion signature.

Since then, a lock-free implementation of run_loop has been found. Atomic operations cannot fail with an exception, so an atomic run_loop can never complete with an error.

[2026-01-16; Reflector poll.]

Set priority to 2 after reflector poll.

"Very helpful to know if something completes without exceptions, it affects whether completions on some types of scheduler are correct."

"Want confirmation from LEWG as run() and finish() have narrow contracts."

Proposed resolution:

This wording is relative to N5014.

[Drafting note: It should be pointed out that member function finish should be noexcept for other reasons as well, see LWG 4215(i).]

  1. Modify 33.12.1.1 [exec.run.loop.general], class run_loop synopsis, as indicated:

    namespace std::execution {
      class run_loop {
        // 33.12.1.2 [exec.run.loop.types], associated types
        class run-loop-scheduler;                        // exposition only
        class run-loop-sender;                           // exposition only
        struct run-loop-opstate-base {                   // exposition only
          virtual void execute() noexcept = 0;           // exposition only
          run_loop* loop;                                // exposition only
          run-loop-opstate-base* next;                   // exposition only
        };
        template<class Rcvr>
          using run-loop-opstate = unspecified;          // exposition only
          
        // 33.12.1.4 [exec.run.loop.members], member functions
        run-loop-opstate-base* pop-front() noexcept;     // exposition only
        void push-back(run-loop-opstate-base*) noexcept; // exposition only
        
      public:
        // 33.12.1.3 [exec.run.loop.ctor], constructor and destructor
        run_loop() noexcept;
        run_loop(run_loop&&) = delete;
        ~run_loop();
        
        // 33.12.1.4 [exec.run.loop.members] member functions
        run-loop-scheduler get_scheduler() noexcept;
        void run() noexcept;
        void finish() noexcept;
      };
    }
    
  2. Modify 33.12.1.2 [exec.run.loop.types] as indicated:

    class run-loop-sender;
    

    -5- run-loop-sender is an exposition-only type that satisfies sender. completion_signatures_of_t<runloop-sender> is

    completion_signatures<set_value_t(), set_error_t(exception_ptr), set_stopped_t()>
    

    […]

    -9- Let o be a non-const lvalue of type run-loop-opstate<Rcvr>, and let REC(o) be a non-const lvalue reference to an instance of type Rcvr that was initialized with the expression rcvr passed to the invocation of connect that returned o. Then:

    • (9.1) — […]
    • (9.2) — […]
    • (9.3) — The expression start(o) is equivalent to:
      try {
        o.loop->push-back(addressof(o));
      } catch(...) {
        set_error(std::move(REC(o)), current_exception());
      }
      
  3. Modify 33.12.1.4 [exec.run.loop.members] as indicated:

    run-loop-opstate-base* pop-front() noexcept;
    

    -1- Effects: Blocks (3.6 [defns.block]) until one of the following conditions is true: […]

    void push-back(run-loop-opstate-base* item) noexcept;
    

    -2- Effects: Adds item to the back of the queue and increments count by 1.

    -3- Synchronization: This operation synchronizes with the pop-front operation that obtains item.

    run-loop-scheduler get_scheduler() noexcept;
    

    -4- Returns: An instance of run-loop-scheduler that can be used to schedule work onto this run_loop instance.

    void run() noexcept;
    

    -5- Preconditions: is either starting or finishing.

    -6- Effects: If state is starting, sets the state to running, otherwise leaves state unchanged. Then, equivalent to:

    while (auto* op = pop-front()) {
      op->execute();
    }
    

    -7- Remarks: When state changes, it does so without introducing data races.

    void finish() noexcept;
    

    -8- Preconditions: state is either starting or running.

    -9- Effects: Changes state to finishing.

    -10- Synchronization: finish synchronizes with the pop-front operation that returns nullptr.