#2579·drogon

SIGSEGV when exception thrown during coroutine promise aggregate initialization (Clang 21 / nlohmann::json)

Author: DreamDonghaoCreated Sep 2, 2026Updated Sep 3, 2026

Describe the bug

A drogon::Task<T> coroutine that takes a parameter implicitly convertible to T crashes with SIGSEGV at the call site — before a single line of the coroutine body executes — when that conversion throws. The process dies inside _Unwind_Resume while unwinding from the partially constructed coroutine frame; the exception is never catchable.

The most common real-world trigger is nlohmann::json: its implicit operator ValueType() calls get<ValueType>(), which throws nlohmann::detail::type_error.302 for mismatched types. We first hit this in production: a bot process terminated silently right after invoking a coroutine tool handler whose argument is a JSON object (Task<std::string> taking const json&). Nothing was logged — the process simply vanished.

Root cause

  1. drogon::Task<T>::promise_type (lib/inc/drogon/utils/coroutine.h) is an aggregate — no user-declared constructors — and its first data member is std::optional<T> value.
  2. Clang 21 aggregate-initializes the promise from the coroutine arguments, positionally: the first argument initializes value, so any implicit conversion to T runs during promise construction — before the body and before initial_suspend.
  3. If that conversion throws, unwinding from the partially constructed coroutine frame faults in _Unwind_Resume.

Crash report excerpt from the minimal repro below:

Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
0  libunwind.dylib  _Unwind_Resume +228
1  repro_min        byRef(FailConvert const&) +1060   <- coroutine ramp (promise init); body never entered
2  repro_min        main +136

To Reproduce

Environment: macOS 26.5.2 (arm64), Apple clang 21.0.0 (clang-2100.1.1.101), drogon 1.9.13 (Homebrew). C++20 suffices; no third-party library needed — the repro uses a 5-line stand-in type.

cpp
// repro.cpp
#include <drogon/utils/coroutine.h>
#include <cstdio>
#include <stdexcept>
#include <string>

// Minimal stand-in for nlohmann::json: implicitly convertible to std::string,
// and the conversion throws at runtime.
struct FailConvert
{
    operator std::string() const
    {
        printf("  converting argument -> std::string (during promise init!)\n");
        std::fflush(stdout);
        throw std::runtime_error("conversion failed");
    }
};

drogon::Task<std::string> byRef(const FailConvert v)
{
    co_return "ok";  // never reached on Clang 21
}

drogon::Task<std::string> byPtr(const FailConvert *v)
{
    co_return "ok";
}

int main(int argc, char **argv)
{
    FailConvert v;
    try
    {
        if (argc > 1)
        {
            auto t = byPtr(&v);      // pointer: no conversion, promise default-constructed
            printf("pointer param   : ok\n");
        }
        else
        {
            auto t = byRef(v);       // reference: Clang 21 aggregate-inits promise.value from it
            printf("reference param : ok\n");
        }
    }
    catch (const std::exception &e)
    {
        printf("caught: %s\n", e.what());
    }
    return 0;
}
bash
clang++ -std=c++20 repro.cpp -I/opt/homebrew/include -o repro   # adjust include path to your drogon

./repro
#   converting argument -> std::string (during promise init!)
# Segmentation fault: 11   (exit code 139; the catch in main is never reached)

./repro ptr
# pointer param   : ok

Per the standard C++20 rules, when no promise constructor takes the coroutine parameters the promise is default-constructed — that is what the reference variant should do (earlier compilers behave this way; only Clang 21 is available in this environment, so we did not re-verify older compilers ourselves).

Expected behavior

A conversion that throws during promise initialization must not crash the process with SIGSEGV. Either the exception should propagate cleanly to the caller after the coroutine frame is properly destroyed, or — as prescribed when no promise constructor takes the parameters — the promise should be default-constructed and the coroutine body should run.

Desktop (please complete the following information):

  • OS: macOS 26.5.2 (arm64, Build 25F84)
  • Compiler: Apple clang 21.0.0 (clang-2100.1.1.101), /usr/bin/c++
  • Drogon Version: 1.9.13 (Homebrew); still present on master (lib/inc/drogon/utils/coroutine.h has no user-declared promise constructor)
  • nlohmann/json: 3.12.0 (real-world trigger, used in the production project where this was found)

Additional context

Minimal library-side fix: add a user-declared default constructor to promise_type so it is no longer an aggregate, and Clang 21+ falls back to default construction (same semantics as pre-Clang-21 compilers):

diff
--- a/lib/inc/drogon/utils/coroutine.h
+++ b/lib/inc/drogon/utils/coroutine.h
@@
     struct promise_type
     {
+        // A user-declared constructor makes promise_type a non-aggregate, so
+        // Clang 21+ will not aggregate-initialize `value` from the first
+        // coroutine argument (see #2579).
+        promise_type() = default;
+
         Task<T> get_return_object()
         {
             return Task<T>{handle_type::from_promise(*this)};
         }

Verified locally: with these lines added to Task<T>::promise_type and Task<void>::promise_type (the latter's first member is std::exception_ptr — same latent hazard), the repro prints reference param : ok on Clang 21. AsyncTask::promise_type has no data members and is not affected. The same helper type makes for a simple regression test: invoking byRef(v) from a test body is enough — with the bug the process segfaults before the next line; with the fix the coroutine behaves normally.

In our project we worked around it by taking const json* instead of const json& in all such coroutine signatures; we would rather revert that once a fixed version ships.

There may also be a Clang-side issue in unwinding from a partially constructed coroutine frame (the SIGSEGV is inside _Unwind_Resume), but the library-side fix above sidesteps the situation entirely. I'm happy to submit a PR with this change.