Skip to content

Bug: <experimental/coroutine> will be deprecated in MSVC Build Tools 14.51 #1520

Description

Version

master

Summary

I work on the Visual C++ team, where we regularly build popular open-source projects, including cppwinrt, with development versions of the MSVC Build Tools in order to find and fix regressions before they can ship and cause trouble for you. This also allows us to provide advance notice of breaking changes, which is the case here.

In the MSVC Build Tools 14.50 shipping in Visual Studio 2026 18.0, the /await compiler option is officially being deprecated. This is a command-line deprecation warning that does not break builds (even under /WX):

C:\Temp>type meow.cpp
int main() {}

C:\Temp>cl /EHsc /nologo /W4 /await meow.cpp
cl : Command line warning D9047 : option 'await' has been deprecated and will be removed in a future release.
    Standard C++ coroutines are available by default in C++20 or later, or use '/await:strict' in earlier language modes.
meow.cpp

In the MSVC Build Tools 14.51 shipping in some future update to Visual Studio 2026, I am additionally deprecating the <experimental/coroutine>, <experimental/resumable>, and <experimental/generator> headers, as the first two have been superseded by C++20 <coroutine> and the third has been superseded by C++23 <generator>. This is a "hard deprecation" implemented as a static_assert. It can be acknowledged and silenced with an escape hatch macro, but future removal is coming. (PR: microsoft/STL#5804 )

We've found that cppwinrt conditionally includes <experimental/coroutine> in an unusual way. It begins by detecting support for Standard coroutines and prefers to include <coroutine> (good), but otherwise falls back to including <experimental/coroutine>. Interestingly, this is guarded by __has_include(<experimental/coroutine>) (which is always true, at least until I physically remove the header in the future), but not by detecting the /await compiler option (which is indicated by the _RESUMABLE_FUNCTIONS_SUPPORTED predefined macro):

#ifdef __cpp_lib_coroutine
#include <coroutine>
namespace winrt::impl
{
template <typename T = void>
using coroutine_handle = std::coroutine_handle<T>;
using suspend_always = std::suspend_always;
using suspend_never = std::suspend_never;
}
#elif __has_include(<experimental/coroutine>)
#include <experimental/coroutine>
namespace winrt::impl
{
template <typename T = void>
using coroutine_handle = std::experimental::coroutine_handle<T>;
using suspend_always = std::experimental::suspend_always;
using suspend_never = std::experimental::suspend_never;
}
#else
#error C++/WinRT requires coroutine support, which is currently missing. Try enabling C++20 in your compiler.
#endif

This has affected several projects in our Real World Code test suite (MuseScore which uses cppwinrt, Qgroundcontrol which uses qtspeech which uses cppwinrt, Qt6 which uses cppwinrt). Interestingly, these projects don't appear to be compiling with deprecated /await - they are simply dragging in cppwinrt, which drags in <experimental/coroutine>, which now triggers my hard deprecation error. (I believe that previously, the experimental coroutine machinery wouldn't actually have been usable without /await, but nothing was trying to use it.)

I am not exactly sure how cppwinrt should change. I think there are a few options:

Option 1: Require Standard coroutines

You could simply require <coroutine>, drop the codepath for <experimental/coroutine>, and adjust your error message that currently says "Try enabling C++20 in your compiler." to additionally mention that /await:strict can be used to opt-in to Standard coroutines even for C++14/17 mode (this is less intrusive for projects that have difficulty quickly migrating to C++20).

Option 2: Detect deprecated /await

Instead of __has_include(<experimental/coroutine>), check for _RESUMABLE_FUNCTIONS_SUPPORTED. This would block "plain vanilla" usage of cppwinrt (i.e. usage in C++14/17 mode with neither /await nor /await:strict), forcing users to choose either the Standard or the deprecated path. Again it would be good to adjust the error message to mention /await:strict.

Option 3: Detect deprecated /await, otherwise don't error

Again check for _RESUMABLE_FUNCTIONS_SUPPORTED, but instead of emitting a compiler error for users in plain vanilla C++14/17 mode, simply don't include anything. It appears that several projects are managing minimal usage of cppwinrt without actually needing coroutines, as indicated by how they were compiling in plain vanilla C++14/17 mode, dragging in cppwinrt, which included <experimental/coroutine> (before my hard deprecation error), and then did nothing with it that actually needed coroutine support. I am not exactly sure if other code would need to be adjusted, but this is potentially the least invasive option for third-party projects that could allow them to continue compiling without needing to add /await:strict (or migrate to C++20).

Macro detection example

For completeness, here is how MSVC's macros work:

C:\Temp>type meow.cpp
#include <cstdio>

int main() {
#ifdef _RESUMABLE_FUNCTIONS_SUPPORTED
    std::puts("Detected the deprecated /await compiler option.");
#endif

#ifdef __cpp_impl_coroutine
#if _MSVC_LANG > 201703L
    std::puts("Detected support for standard coroutines, available in C++20.");
#else
    std::puts("Detected support for standard coroutines, available with /await:strict in C++14/17.");
#endif
#endif

#if !defined(_RESUMABLE_FUNCTIONS_SUPPORTED) && !defined(__cpp_impl_coroutine)
    std::puts("Detected nothing.");
#endif
}
C:\Temp>(cl /EHsc /nologo /W4 /std:c++14 meow.cpp > NUL) && meow
Detected nothing.

C:\Temp>(cl /EHsc /nologo /W4 /std:c++14 /await meow.cpp > NUL) && meow
cl : Command line warning D9047 : option 'await' has been deprecated and will be removed in a future release.
    Standard C++ coroutines are available by default in C++20 or later, or use '/await:strict' in earlier language modes.
Detected the deprecated /await compiler option.

C:\Temp>(cl /EHsc /nologo /W4 /std:c++14 /await:strict meow.cpp > NUL) && meow
Detected support for standard coroutines, available with /await:strict in C++14/17.

C:\Temp>(cl /EHsc /nologo /W4 /std:c++20 meow.cpp > NUL) && meow
Detected support for standard coroutines, available in C++20.

C:\Temp>(cl /EHsc /nologo /W4 /std:c++20 /await meow.cpp > NUL) && meow
cl : Command line warning D9047 : option 'await' has been deprecated and will be removed in a future release.
    Standard C++ coroutines are available by default in C++20 or later, or use '/await:strict' in earlier language modes.
Detected the deprecated /await compiler option.

C:\Temp>(cl /EHsc /nologo /W4 /std:c++20 /await:strict meow.cpp > NUL) && meow
Detected support for standard coroutines, available in C++20.

(Here I'm using __cpp_impl_coroutine to detect compiler support for Standard coroutines, but it's fine and probably better for cppwinrt to use our library macro __cpp_lib_coroutine.)

Reproducible example

Expected behavior

No response

Actual behavior

No response

Additional comments

No response

Activity

  1. StephanTLavavej commented on Nov 4, 2025

    @StephanTLavavej
    MemberAuthor

    We've additionally found places where CppWinRT is passing deprecated /await:

    <BoolProperty Name="CppWinRTEnableLegacyCoroutines"
    DisplayName="Enable legacy coroutines (C++17)"
    Description="Enables the /await compiler option (disable this if you want to pass this yourself)"
    Category="General" />

    <AdditionalOptions Condition="'%(ClCompile.LanguageStandard)' == 'stdcpp17' And '$(CppWinRTEnableLegacyCoroutines)' != 'false'">%(AdditionalOptions) /await</AdditionalOptions>

  2. kennykerr commented on Nov 4, 2025

    @kennykerr
    Collaborator

    I don’t work on the C++ tooling much anymore, but I just wanted to point out that all these wonky workarounds are ancient and predate much of the coroutine standardization process. I doubt there’s any danger in simply ripping it all out at this point (Option 1).

  3. dmachaj commented on Nov 5, 2025

    @dmachaj
    Contributor

    cpp20 isn't exactly new at this point. I would also be supportive of removing pre-standardization experimental coroutine support. My vote would be that cpp20 gets standard coroutines and that cpp17 gets zero coroutines by default (with a possible option to opt into /await:strict).

    We don't need to worry about cpp14 consumers because the minimum language version for cppwinrt is cpp17.

  4. sylveon commented on Nov 6, 2025

    @sylveon
    Contributor

    /await:strict can act as the option itself for the opt-in. Just have cppwinrt handle #ifdef __cpp_lib_coroutine and you'll get this exact behavior.

    Maybe if you wanna be nice you can add an #error or #warning for people that still use /await.

  5. DefaultRyan commented on Nov 6, 2025

    @DefaultRyan
    Member

    Thanks for the heads up, Stephan T. Lavavej (@StephanTLavavej)! We need to start preparing right away...

    There are many projects in the OS using cppwinrt with /await where I'm confident the default is still cpp17 rather than cpp20. I'm not holding my breath on getting all those projects migrated to cpp20 soon, so let's preserve cpp17 via /await:strict and get the OS projects builds to default to /await:strict.

    Unfortunately, I also count hundreds of projects that will need to be updated because they have manually added /await. Let's start a thread with the LKG tool ingestion folks and find out how far back we have support for /await:strict and other logistical considerations.

  6. sylveon commented on Nov 6, 2025

    @sylveon
    Contributor

    I opened #1521 to fix this. It swaps out /await for /await:strict by default on C++17, which can be observed by all the existing tests still building fine :)

    For the internal projects that need to be updated, I can't help however (maybe one day...)

  7. DefaultRyan commented on Nov 6, 2025

    @DefaultRyan
    Member

    I opened #1521 to fix this. It swaps out /await for /await:strict by default on C++17, which can be observed by all the existing tests still building fine :)

    For the internal projects that need to be updated, I can't help however (maybe one day...)

    In light of the big and potentially length migration process, I'm favoring Option 2 temporarily. We should update the "no coroutines" error message as suggested. Also consider an informative warning (with escape hatch) for when the legacy option is detected, to warn consumers to start migrating.

    In other words, deprecate cppwinrt's support for legacy coroutines, but give consumers time to migrate before we turn off support altogether by adopting Option 1.

    We should also look into some sort of deprecation or update for $(CppWinRTEnableLegacyCoroutines) added to Microsoft.Windows.CppWinRT.targets in #1015. The targets file in the NuGet adds /await by default. I don't have telemetry to tell me whether consumers are predominantly using this behavior, or if they are opting out and controlling that flag themselves. But a quick search on GitHub of CppWinRTEnableLegacyCoroutines only turned up a few hits while Microsoft.Windows.CppWinRT turned up 6300 hits, and adding await to the search lowered that number to ~500. So that suggests the vast majority of consumers are using the automatic behavior and just want things to work. Removing the automatic /await completely is the kind of widely disruptive breaking change that we'd reserve for a major version. Perhaps we should simply update the automatic option to supply /await:strict in the meantime.

  8. sylveon commented on Nov 7, 2025

    @sylveon
    Contributor

    I'm not sure I get the reasoning for option 2. Users can simply hold off updating C++/WinRT until they are ready to get off legacy coroutines, since it is a NuGet package.

    Agreed that we could add a better warning here (maybe even make it an #error), but I don't feel an escape hatch for the warning is needed for the same reason as above. Not ready to upgrade yet? Stay on the old package (you'll have to stay on an old compiler too anyways).

    Just doing

    #if defined(__cpp_lib_coroutine)
    #include <coroutine>
    #elif defined(_RESUMABLE_FUNCTIONS_SUPPORTED)
    #warning "Legacy coroutines are no longer supported, please upgrade to /std:c++20 or /await:strict
    #endif

    should be plentiful.

    But a quick search on GitHub of CppWinRTEnableLegacyCoroutines only turned up a few hits

    Worth pointing out that these hits are all disabling it. For these people, the end behavior stays the same, C++/WinRT will just start passing /await:strict for them (which they where already doing).

    So that suggests the vast majority of consumers are using the automatic behavior and just want things to work. Removing the automatic /await completely is the kind of widely disruptive breaking change that we'd reserve for a major version.

    The new behavior of the PR for those customers, is that they would get automatically upgraded to C++20 coroutines, since CppWinRTEnableCpp17Coroutines defaults to true. So things still just work.

    The one edge case I could think of, is people with <CppWinRTEnableLegacyCoroutines>true</CppWinRTEnableLegacyCoroutines> would start getting /await:strict instead of /await, but I can't find anybody actually doing or wanting that, especially since the option got added when the new standard coroutines got backported to C++17.

  9. DefaultRyan commented on Nov 7, 2025

    @DefaultRyan
    Member

    I'm not sure I get the reasoning for option 2. Users can simply hold off updating C++/WinRT until they are ready to get off legacy coroutines, since it is a NuGet package.

    Holding off on updating means also holding off on other bug fixes and improvements, some of which may be important or urgent. Some projects are huge and messy and might take considerable effort to manually clean all the legacy /await options that have crept in. Not all who consume C++/WinRT do so through NuGet, but still want the updated codegen.

    As one small example, some customers consume the C++/WinRT headers that are distributed in the Windows SDK instead of using NuGet. If we completely rip out legacy coroutine support, all those customers that are using legacy /await will break when they get a new SDK. Ditto for many internal Windows OS projects when I need to bring the next version of C++/WinRT into some of our internal builds.

    Instead, I'm proposing we keep the legacy support for now, but with the minor cleanup in the codegen to not rely solely on __has_include(<experimental/coroutine>), but instead check for the actual presence of /await before including the legacy headers. As part of this, we also improve the error message for "no coroutine support detected" and optionally introduce a build warning (with escape hatch) when legacy support is included to warn users of the impending toolset deprecation and suggest migrating to C++/20 or /await:strict.

    This gives customers time to clean their stuff up and/or quickly revive their CI builds when suddenly break overnight because a GitHub or Azure DevOps build runner incremented the version of the Windows SDK that is preinstalled on their image. (Yes, CI build runner images update this kind of thing (Windows SDK version) with no action on the user's end, and I'd like to limit the potential destruction).

    Worth pointing out that these hits are all disabling it. For these people, the end behavior stays the same, C++/WinRT will just start passing /await:strict for them (which they where already doing).

    I saw a mix of false and true hits in the search. Changing the name from CppWinRTEnableLegacyCoroutines to CppWinRTEnableCpp17Coroutines means folks who had set it to false won't inherit that old value, and the new default will suddenly start adding stuff to their command line again. We don't have to change the property name when we can update the display name and description. The name is unfortunate, but it's probably better than churning the setting (unless we put this behind a major version update).

    Furthermore, if we change the default to /await:strict, then the non-trivial number of projects that had manually added /await will break. Before, they were getting /await twice: once from the NuGet injection, and once from their vcxproject/props, which is a silly when you look at the command line, but benign. Now, they'll get /await:strict from the NuGet added to their /await in their vcxproject/props. This fails to compile with

     cl : command line  error D8016: '/await:strict' and '/await' command-line options are incompatible.
    

    Maybe that amount of breakage is worth it, compared to kicking the can down the road for the rest of the users by leaving the default as the soon-to-be-deprecated option, but we should at least be aware that this will impact a number of customers.

  10. sylveon commented on Nov 9, 2025

    @sylveon
    Contributor

    Changing the default is definitely desirable I think - most users do not set that property and rely on the implicit behavior, never manually invoking std::experimental stuff. We should favor these usages getting auto-upgraded over not breaking the minority of projects that might be using it, IMO. There is more breakage to be seen by keeping the majority of users on /await and those getting an update of their compiler in CI.

    I'm not a big fan of the escape hatch, because as you've mentioned it allows people to quickly bring CI back up. In my experience when this happens, people set the escape hatch and forget about it, kicking the can down the road for when it's fully removed. I'm having this exact thing at work right now, where a compiler upgrade introduced new warnings we had to disable and I'm the only one driving the effort to properly fix them so we can re-enable them (but I keep getting distracted with higher priority work).

    In this case migration is fairly easy, there is no API differences so you can get away with a simple find and replace from std::experimental to std (if you've even used anything from there - which is unlikely). Considering that, I think it's better to encourage early migration here - if you use /Wx then you've asked for the random CI upgrades breaking things anyways :)

    I'm not sure there's an easy solution here that wouldn't break some amount of explicit usages, but also auto-upgrade implicitly. For example, changing the default to false, introducing a new option for /await:strict and defaulting that to true hits the same D8016 error you mention if someone had set the legacy option to true (and those users probably would've preferred the auto-upgrade behavior anyways).

    I'd be curious to see how many projects break when using my PR in an internal Windows build - I can imagine most of the breakages would be an error because they've explicitly passed /await somewhere and C++/WinRT now also passes /await:strict - there is probably not many projects using CppWinRTEnableLegacyCoroutines since it's fairly recent all things considered. If there was an escape hatch, you'd have to modify all those project files to silence the warning, but I would bet that just fixing the /await would get most (all?) projects building again anyways.

    Either way, I can keep the PR up for when you guys are ready to fully tear it off. Kenny seemed in favor of that.

  11. github-actions commented on Nov 20, 2025

    @github-actions
  12. StephanTLavavej commented on Nov 20, 2025

    @StephanTLavavej
    Author
  13. kennykerr commented on Nov 20, 2025

    @kennykerr
  14. 7 remaining items

  15. StephanTLavavej commented on Jan 16, 2026

    @StephanTLavavej
    Author
  16. DefaultRyan commented on Feb 3, 2026

    @DefaultRyan
  17. sylveon commented on Feb 3, 2026

    @sylveon
  18. github-actions commented on Feb 18, 2026

    @github-actions
  19. github-actions commented on Mar 1, 2026

    @github-actions
  20. hoshiizumiya commented on Mar 1, 2026

    @hoshiizumiya
  21. DefaultRyan commented on Mar 13, 2026

    @DefaultRyan
    Member

    Update. I've gotten some extra help to migrate the deprecated /await to /await:strict in Windows, and I feel far better about ripping off support more actively. (Option 1). I'm going to try and move this forward now, and if we hit bumps, I'll switch to Option 2 and add an escape hatch to opt in support for the legacy experimental coroutines.

  22. DefaultRyan commented on Mar 15, 2026

    @DefaultRyan
    Member

    Almost there. Charles Milette (@sylveon) Worked on some nice coroutine-less fallback logic because the current C++/WinRT was overeagerly including the experimental headers, convincing itself that it had coroutine support even when no command-line option was supplied. This means we almost certainly had some number of consumers using C++/WinRT without supplying /await, and as long as they never uttered co_await, their programs blissfully compiled and ran. (C++/WinRT doesn't generate any code that uses co_await, it merely defines the machinery that allows consumers to do so)

    Once we remove the overeager include, these consumers would suddenly break if the strengthened coroutine detection logic started emitting errors. I wanted to see what would happen if these consumers started sprinkling some co_awaits into their code...

    Stephan T. Lavavej (@StephanTLavavej) may be amused to learn there is an error message seemingly embedded in the compiler itself that still refers to the old deprecated experimental stuff. (I'm assume it's coming from the front end, and not anywhere in the STL)

    https://godbolt.org/z/5zGGGqr3Y

    <source>(10): error C3773: Use of 'co_await' in this context is a non-conforming extension in C++17
    <source>(10): note: Please use '/await' command-line option to enable relevant extensions
    <source>(10): error C3774: cannot find 'std::experimental': Please include <experimental/coroutine> header
    

    Image
    I've filed a Developer Community issue for this, hoping to remove this additional tripping hazard for all users (not just C++/WinRT users) as we drag gently encourage them away from their legacy coroutines.

    https://developercommunity.visualstudio.com/t/Error-message-suggests-experimental-aw/11060319

  23. StephanTLavavej commented on Mar 15, 2026

    @StephanTLavavej
    MemberAuthor

    I took a brief look, but more compiler changes may be necessary than a simple message fix so I'll let a real compiler dev investigate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions