Blog, by Gonçalo

I Write About Programming and maybe more

Published: 30 August 2025

C++ Coroutines — In Depth 🔎

by Gonçalo Ferreira

Table of Contents

Some Quick Basics

This is probably not the first post you read about coroutines, so let’s quickly run through the basics:

A Beautiful and Simple Coroutine - std::generator

Before diving deep, let’s look at a very nice coroutine. (If you don’t think it’s nice, wait until you see what’s coming next.)

Don’t worry about any details here, just look at how nice it is to write and use.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <print>
#include <generator> // since C++23

std::generator<int> fibonacci_numbers() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;
        auto next = a + b;
        a = b;
        b = next;
    }
}

int main() {
    auto fib = fibonacci_numbers();
    for (auto num : fib) { // infinite loop
        std::println("{}", num);
    }
    return 0;
}

First, we know fibonacci_numbers() is a coroutine because it has the co_yield keyword somewhere inside.

In coroutines lingo, it’s called a generator. Essentially, it’s a coroutine that keeps outputting (yielding) values.

Something that might be confusing is line 15. It looks like a function call, but remember that fibonacci_numbers() is not really a function - it’s a coroutine. In reality, that line is lazy, it does not execute any code inside fibonacci_numbers(), it only returns a sort of handle that we can use later to interact with the coroutine (like we do in line 16).

Also, notice how we can use the fib handle as a range, pretty cool.

By the end of this post, you will know how to implement your own std::generator.

Our First Coroutine

Let’s start simple:

1
2
3
4
5
6
7
8
9
10
11
12
#include <print>

void f() {
    std::println("Hello coro");
    co_return;
}

int main() {
    std::println("Hello main");
    auto handle = f();
    return 0;
}

f() is a coroutine because it has the co_return keyword inside. Now, if we try to compile it we get an error (with a nice message).

basic_coro_compilation_error

It tells us it cannot find the promise type for this coroutine. It’s not that obvious what this means. However, it’s a crucial aspect of coroutines in C++.

For coroutines to work, they must have a very specific return type.

In specific, they must return a type that has a type named exactly promise_type. Why? Because yes. Simply by definition: it’s what the language asks from us.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <print>

struct MySpecialType {
  struct promise_type {};
};

MySpecialType f() {
    std::println("Hello coro");
    co_return;
}

int main() {
    std::println("Hello main");
    auto handle = f();
    return 0;
}

Let’s compile it again. Now we get a slightly less pretty error message. It complains about our promise type not implementing the methods:

Again, the compiler needs more from us: we must define these methods. One big question is: what should their signature be? Or maybe, first we should ask what their purpose is.

Let’s discuss each one: (the line numbers mentioned refer to the code above)

Just a quick note about the term coroutine frame. The state of a coroutine lives on the heap. Where state is essentially what the stack frame of a normal function would be.

Our new code is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <coroutine> // needed for std::suspend_always and std::suspend_never

struct MySpecialType {
    struct promise_type {
        MySpecialType get_return_object() { return MySpecialType{*this}; }
        void return_void() noexcept {}
        std::suspend_always initial_suspend() noexcept { return {}; }
        void unhandled_exception() noexcept {}
        std::suspend_always final_suspend() noexcept { return {}; }
    };

    MySpecialType(promise_type&) {}
};

MySpecialType f() {
    std::println("Hello coro");
    co_return;
}

int main() {
    std::println("Hello main");
    auto handle = f();
    return 0;
}

This compiles, and the output is:

Hello main

Notice the return type of get_return_object(), that will be the type of handle in line 22.

Now let’s address the (maybe) cryptic std::suspend_always. It’s counterpart is std::suspend_never. We can understand what they mean very easily by doing one small change. Let’s change the return type of the initial_suspend() method to std::suspend_never. Now, if we run it again we get the output:

Hello main
Hello coro

With std::suspend_never, we are telling the compiler to start executing the coroutine immediatelly when it is called (much like how it works with functions).

In other words, in this case: std::suspend_never gives the execution flow to the body of the coroutine. While std::suspend_always keeps the flow where it is.

This might be a good time to address final_suspend(), since at least to me it is not as easy to grasp as inital_suspend().

As we mentioned before, final_suspend() is called when the coroutine ends, i.e falls out of scope on line 18 in the code above. Since we are used to functions, we are used to thinking that when the end of the scope is reached, everything in the function stack gets cleaned up and that is that. The same is not entirely true to coroutines.

Remember that coroutines do not have a stack frame, they have a coroutine frame that lives on the heap memory (not the stack memory).

Now it makes more sense that the compiler asks us for this final_suspend() method. Since the coroutine frame lives on the heap, what should the compiler do with this frame once the coroutine ends? Should it free it (i.e deallocate the memory) or keep it alive?

If we want the coroutine frame to be kept alive, then we use std::suspend_always. Otherwise, we use std::suspend_never. This may not seem like a big deal now, and in our current code it actually is not. But we will see later that it does matter (of course!).

For now, take a break, grab a coffee ☕️, and drink it while we appreciate how much control the language gives us over our coroutine.

Yielding a Value Out Of a Coroutine - co_yield

We now know the fundamentals. It’s time to write a slightly cooler coroutine:

MySpecialType f() {
    co_yield "Hello coro";
}

Now we don’t just print, we yield. Meaning that we want the caller of the coroutine to receive this value - much like when a function returns a value.

Also, note that we removed the co_return statement. Along with it, we also removed the return_void() method (and the compiler did not complain). f() is still a coroutine because it has the co_yield keyword.

Okay, let’s try to compile:

basic_coro_compilation_error

We’ve dealt with this problem before. Let’s add this method to our promise type.

  std::suspend_always yield_value(std::string) noexcept { return {}; }

This will be accepted by the compiler. Since we want to co_yield a string, we should have it be the parameter of yield_value(). Lastly, with the return type we indicate wether after the co_yield the coroutine continues to execute (std::suspend_never) or if it gives control back to the caller (std::suspend_always). In our case, it will give back control.

The compiler will also accept other signatures, for example:

  std::suspend_always yield_value(std::string_view) noexcept { return {}; }
  std::suspend_always yield_value(const char*) noexcept { return {}; }

If we compile again, it works.

But… How does the caller access the yielded value? Our type MySpecialType must get it from the promise type.

struct MySpecialType {
    struct promise_type {
      // ...
    };

    using HandleType = std::coroutine_handle<promise_type>;
    HandleType coro_handle_{};

    explicit MySpecialType(promise_type& prom)
      : coro_handle_{HandleType::from_promise(prom)} {}

    ~MySpecialType() {
        if (not coro_handle_.done()) coro_handle_.destroy();
    }
};

First, we must have some sort of handle in our MySpecialType that references the promise. For our purposes, coroutine promise and coroutine frame are sort of the same thing: we can think of it as a pointer to the coroutine’s data. (In fact, std::coroutine_handle<T> is essentially a pointer to the coroutine frame).

Next, let’s hold the yielded value in our promise type. Why? Because with our coro_handle_ we can get to the promise type - by doing coro_handle_.promise().{field}.

This is how our promise type looks now: (notice the field out_ and the method yield_value())

struct promise_type {
    std::string out_;

    MySpecialType get_return_object() { return MySpecialType{*this}; }
    std::suspend_always initial_suspend() noexcept { return {}; }
    
    std::suspend_always yield_value(std::string val) noexcept {
        out_ = std::move(val);
        return {};
    }

    void unhandled_exception() noexcept {}
    std::suspend_always final_suspend() noexcept { return {}; }
};

Finally, let’s add a method in MySpecialType to retrieve this out_ value from the promise. We can call this method anyting we want, I will call it get().

The final code is this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
struct MySpecialType {
    struct promise_type {
        std::string out_;

        MySpecialType get_return_object() { return MySpecialType{*this}; }
        std::suspend_always initial_suspend() noexcept { return {}; }
        
        std::suspend_always yield_value(std::string val) noexcept {
            out_ = std::move(val);
            return {};
        }

        void unhandled_exception() noexcept {}
        std::suspend_always final_suspend() noexcept { return {}; }
    };

    using HandleType = std::coroutine_handle<promise_type>;
    HandleType coro_handle_{};

    explicit MySpecialType(promise_type& prom)
      : coro_handle_{HandleType::from_promise(prom)} {}

    ~MySpecialType() {
        if (not coro_handle_.done()) coro_handle_.destroy();
    }

    std::string get() { // may have any name
        if (not coro_handle_.done()) coro_handle_.resume();
        return std::move(coro_handle_.promise().out_);
    }
};

MySpecialType f() {
    co_yield "Hello coro";
}

int main() {
    std::println("Hello main");
    auto handle = f();
    std::println("Coroutine says: {}", handle.get());
    return 0;
}

The output is:

Hello main
Coroutine says: Hello coro

An explanation is due about line 28. Mainly, why do we call resume on the coroutine? We actually already have all the knowledge to know why. Let’s go through it step by step:

I will take this chance to show you the importance of the return type of final_suspend(). Let’s imagine changing all std::suspend_always in the code above with std::suspend_never. Now, when we call the coroutine in line 39, it will:

  1. immediately start executing
  2. when it hits the co_yield it will just keep on going to the next line
  3. when it reaches the end of the scope in line 35 it will deallocate the coroutine frame in the heap

Essentially, it will behave like a function. So, what is the problem? Well, now in line 40 when we call handle.get() to fetch the value, we will get a SEGMENTATION FAULT - 2025 and still hitting these… 😑

Why? Because the coroutine frame no longer exists. A few paragraphs ago, we said that coro_handle_ is essentially a pointer to the coroutine frame. If this frame no longer exists, we should not use coro_handle_ anymore. And indeed the frame has been destroyed, because our final_suspend() method returns std::suspend_never, which tell the coroutine not to suspend/pause.

If we keep the coroutine frame alive, by only changing the return type of final_suspend() back to std::suspend_always, it works again. Like so:

std::suspend_never initial_suspend() noexcept {...}
std::suspend_never yield_value(std::string) noexcept {...}
std::suspend_always final_suspend() noexcept {...}

Now, the end result will be the same as before, but the flow of execution will still be different - as we have just seen.

We deserve another ☕️ break by now.

Revisiting our Basic Coroutine - co_return

Before we move on to the next beast, let’s quickly look at co_return again. In our first example, the coroutine had a print and simple empty co_return statement, with a return type of void. This forced us to define the return_void() method in our promise type.

What if we wanted to return something else? Let’s try the following setup:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct MySpecialType {
    struct promise_type {
        MySpecialType get_return_object() { return MySpecialType{*this}; }
        // void return_void() noexcept {}
        std::suspend_always initial_suspend() noexcept { return {}; }
        void unhandled_exception() noexcept {}
        std::suspend_always final_suspend() noexcept { return {}; }
    };

    MySpecialType(promise_type&) {}
};

MySpecialType f() {
    std::println("Hello coro");
    co_return 10;
}

int main() {
    std::println("Hello main");
    auto handle = f();
    return 0;
}

If we compile, we get an error:

coreturn_int_compile_error

As we mentioned before, we need a method called return_value instead of return_void:

void return_value(int) noexcept {}

The way we get this value out is identical to how we did it before with co_yield. We need a new int field in our promise type, and a new method in our MySpecialType to read this value from the promise type.

Also, notice that the return type of return_value is just void. This is because after co_return gets executed, the method final_suspend will be called to let the coroutine know if it should suspend or not - so we do not need to specify this behavior in return_value.

Note that we used an int, but of course it can be any type.

The final code looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
struct MySpecialType {
    struct promise_type {
        int out_;
        MySpecialType get_return_object() { return MySpecialType{*this}; }
        void return_value(int x) { out_ = x; }
        std::suspend_always initial_suspend() noexcept { return {}; }
        void unhandled_exception() noexcept {}
        std::suspend_always final_suspend() noexcept { return {}; }
    };

    using HandleType = std::coroutine_handle<promise_type>;
    HandleType coro_handle_{};

    explicit MySpecialType(promise_type& prom)
      : coro_handle_{HandleType::from_promise(prom)} {}

    ~MySpecialType() {
        if (not coro_handle_.done()) coro_handle_.destroy();
    }

    int get() {
        if (not coro_handle_.done()) coro_handle_.resume();
        return coro_handle_.promise().out_;
    }
};

MySpecialType f() {
    std::println("Hello coro");
    co_return 10;
}

int main() {
    std::println("Hello main");
    auto handle = f();
    std::println("Coroutine co_returned {}", handle.get());
    return 0;
}

And the output is:

Hello coro
Coroutine co_returned 10

Before we move on. Let’s revisit our discussion about the importance of the return type of final_suspend. Previously, we established that this method tells the compiler wether to deallocate the coroutine frame or not. Let me add another, and the last piece of information, so we are fully aware of how things work.

Independently of the return type we choose for final_suspend, once the coroutine reaches the end of the scope, all local variables it contains will be destroyed - much like when a function ends.

The devil is in the details: only the state of the promise depends on the return value of final_suspend. The local variables of the coroutine always get cleaned up at the end of the scope.

This is, more often than not, a technicality. But it is good to know. For example, now you know that even if you have std::suspend_always final_suspend() {...}, you should not keep any pointers in your promise to any local local variables in the finished coroutine.

Getting a Value Into the Coroutine - co_await

Finally, we will talk about `co_await’.

As usual, let’s just add a co_await to our code and see what the compiler tells us.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct MySpecialType {
    struct promise_type {
        MySpecialType get_return_object() { return MySpecialType{*this}; }
        std::suspend_always initial_suspend() noexcept { return {}; }
        void unhandled_exception() noexcept {}
        std::suspend_always final_suspend() noexcept { return {}; }
    };

    using HandleType = std::coroutine_handle<promise_type>;
    HandleType coro_handle_{};

    explicit MySpecialType(promise_type& prom)
      : coro_handle_{HandleType::from_promise(prom)} {}

    ~MySpecialType() {
        if (not coro_handle_.done()) coro_handle_.destroy();
    }
};

MySpecialType f() {
    auto in = co_await std::string{};
    std::println("Coroutine received {}", in):
}

int main() {
    std::println("Hello main");
    auto handle = f();
    return 0;
}

Compiling gives us the error:

coawait_compile_error

This one is a bit more cryptic. It indicates that something is missing in the type std::string itself. I was expecting something like:

missing method 'await_value' in promise_type

As it happens, the method we need to define in order to use co_await is slightly less nice than the others. Let’s go step by step. Indeed, we can solve this error by adding another method to our promise type:

auto await_transform(auto) noexcept { ... }

But what should its signature be? This one is hard to guess. But we at least know 2 things:

Before we go any further, let me assure you that if you think this is annoying you are not alone. Why is await not as simple as the other methods? Well, let’s write it the way we want to and try to spot the issue.

std::suspend_always await_value(std::string s) noexcept {
  // what to do with 's' ?
  return {};
}

Remember that when the compiler sees co_await it will replace it by a call to our await_value method, so it should return a std::string. But, if it returns a std::string it cannot return std::suspend_always. This is the problem, we need more than a single function.

The proper way to do it is as follows:

auto await_transform(std::string) noexcept {
    struct awaiter { // we can name this type whatever we want
        promise_type& promise;
        bool await_ready() const noexcept { return true; }
        std::string await_resume() const noexcept { ... }
        void await_suspend(HandleType) const noexcept {}
    };

    return awaiter{*this};
}

We do need all these methods to make co_await_ work. Let’s look at each one individually.

Oh! By the way… We just implemented our own std::suspend_never/std::suspend_always. Really! Check what the standard says about these types: std::suspend_never 🔗 and std::suspend_always 🔗 - note the 3 methods they define. Also, note that the only difference between them is the return value of the method await_ready.

The standard library developers got nothing on us. 😎

Finally, we need to return a string from await_resume. This requires communication between the handle and the coroutine. We have done this before.

The final code is below. Notice the methods MySpecialType::put and await_ready, and what we do in main().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
struct MySpecialType {
    struct promise_type {
        std::string in_;

        MySpecialType get_return_object() { return MySpecialType{*this}; }
        std::suspend_always initial_suspend() noexcept { return {}; }

        auto await_transform(std::string) noexcept {
            struct awaiter {
                promise_type& promise;
                bool await_ready() const noexcept { return true; }
                std::string await_resume() const noexcept {
                  return std::move(promise.in_);
                }
                void await_suspend(HandleType) const noexcept {}
            };

            return awaiter{*this};
        }

        void unhandled_exception() noexcept {}
        std::suspend_always final_suspend() noexcept { return {}; }
    };

    using HandleType = std::coroutine_handle<promise_type>;
    HandleType coro_handle_{};

    explicit MySpecialType(promise_type& prom)
      : coro_handle_{HandleType::from_promise(prom)} {}

    ~MySpecialType() {
        if (not coro_handle_.done()) coro_handle_.destroy();
    }

    void put(std::string s) { // can be named anything
        coro_handle_.promise().in_ = std::move(s);
        if (not coro_handle_.done()) coro_handle_.resume();
    }
};

MySpecialType f() {
    auto val = co_await std::string{};
    std::println("{}", val);
}

int main() {
    std::println("Hello main");
    auto handle = f();
    handle.put("Hello coro");
    return 0;
}

Let’s make sure we really, really understand what is going on here.

Look at line 47: this is a lazy line, because initial_suspend returns std::suspend_always. Look also at the method await_ready: we are returning true, which means that when the execution flow reaches the co_await instruction it will not actually wait. It will immediately invoke await_resume and continue executing - so we must ensure we have already initialized in_ when we reach the co_await.

This is why in put() we first initialize in_, and only then resume (in our case, start) the coroutine.

That’s pretty much it, elementary.

Some More Details So You Can Have Even More Fun

std::suspend_always yield_value(std::string) noexcept { ... }
std::suspend_always yield_value(std::vector) noexcept { ... }

References

Videos on Youtube:

Books:

tags: c++ - coroutines - deep-dive