The concept and environment
C# 13 permits carefully scoped ref-struct locals inside async methods, while preserving the rule that they cannot be used across an await suspension boundary. The sample performs all ReadOnlySpan work in an inner synchronous block, stores an ordinary integer result, then awaits. This is genuine C# 13 source compiled for the .NET 9 environment, not one of the external C# 14 comparisons. The important improvement is safe placement of span-based work, not permission to retain arbitrary stack-only views in a heap-resident async state machine.
Walk through the portable program
SumThenContinueAsync declares an integer total, enters a block, and creates a span over two and three. It computes five while that span is in scope. The block ends before Task.Yield, so only the ordinary integer value is needed afterward. Once the awaited operation continues, the method returns total and the outer await prints sum:5. The result does not depend on a particular thread, elapsed duration, or scheduler speed. Task.Yield supplies an explicit asynchronous boundary without using a real-time sleep as a correctness assumption.
A deeper design boundary
A span is a ref-struct view with lifetime restrictions; its underlying bytes are not necessarily all stack allocated. An array-backed span can describe managed array storage, and a collection expression's permitted storage strategy depends on context and compiler rules. This program does not claim a particular allocation layout. The lexical block makes the view's intended lifetime obvious while transferring only a computed scalar across suspension. For broader processing, a synchronous helper that consumes a span and returns an ordinary result can provide the same clean boundary and support older compiler environments.
Pitfalls and limits
Do not move a later read of values below await and assume the earlier successful declaration made that safe. The compiler enforces the no-crossing restriction; deliberately invalid lifetime experiments should produce diagnostics, not a runtime result. Capturing a span in a lambda or storing it in an ordinary class field remains a separate prohibited escape. If asynchronous work must retain data, use an appropriate owned array or lifetime-aware memory representation and specify who may mutate it. Changing a type to Memory does not by itself invent an ownership policy or freeze a caller's buffer.
Practice one: isolate a contract
First change the span elements to four and six and predict sum:10, leaving its scope and the await boundary unchanged. Then add an empty-input implementation with a loop and an explicit zero identity rather than indexing element zero. Hint: keep lifetime safety and input-boundary correctness as independent requirements. A program can obey every ref-safety rule while still indexing an empty span incorrectly. Test the empty and nonempty behaviors with direct expectations so a compiler success is not mistaken for a proof of the domain algorithm.
Practice two: challenge an assumption
For another exercise, move the synchronous span work into a non-async helper returning int and call that helper before awaiting. Predict the same output while making the boundary reusable. Hint: the helper must consume the view during its call, not retain it in a closure. In a separate nonbaseline source copy, attempt to access the span after await and observe the compiler rejection. Then design a version that deliberately owns an array across suspension, documenting mutation and copying choices instead of treating a successful compilation as evidence that shared data cannot change.
Review the resulting contract
A ref-struct async review should identify the last use of each view, the suspension points, and which ordinary results survive afterward. Compiler safety establishes allowed lifetimes, while independent checks establish the calculation's correctness. The sample satisfies both concerns with a short inner block and one scalar result. Its desktop compilation verifies actual C# 13 acceptance; the parent integration task separately checks the bundled browser compiler and runtime. No external .NET 10 project is needed for this feature, and no claim is made that await preserves a span whose lifetime crosses suspension.
A working example
using System;
using System.Threading.Tasks;
Console.WriteLine($"sum:{await SumThenContinueAsync()}");
async Task<int> SumThenContinueAsync()
{
int total;
{
ReadOnlySpan<int> values = [2, 3];
total = values[0] + values[1];
}
await Task.Yield();
return total;
}
Expected output
sum:5
Keep the span inside the inner block and carry only the integer across await. Move span work into a synchronous helper when supporting older compilers.