The Unity script needs the Editor and the setup listed below. The separate C# companion practice runs in SharpRune without UnityEngine.
Choose the lifetime before awaiting
An asynchronous operation needs both a completion contract and an owner. Unity 6.0 provides Awaitable operations that integrate with engine execution, but the async keyword does not automatically move work to a worker thread. The supplied wait must be called on the main thread and completes there. Its CancellationToken requests cooperative cancellation rather than terminating a thread. Keeping those facts separate prevents a misleading design in which a component assumes every await is parallel work or every inactive object has already canceled everything it previously started.
AsyncLifetimeProbe uses destruction as the cancellation lifetime. It reads destroyCancellationToken while the component exists, then passes that cached token into the wait. Disabling is a different transition and does not request cancellation through this token. After successful completion, the script checks whether the component still exists and is active and enabled before producing its message. That final guard can suppress an unwanted result, but it does not undo completed work, cancel the wait retroactively or schedule a new operation when the component becomes enabled again.
Portable C# companion: canceled Task work
In the SharpRune companion, lifetime.Cancel executes before Task.Delay is awaited. Awaiting with that already-canceled token takes the OperationCanceledException path, which prints cancelled. The completed increment is after the await inside the try block, so it is skipped, and the final counter is zero. These two lines are a deterministic ordinary Task cancellation example. They neither instantiate Unity Awaitable nor reproduce its pooling, continuation or Player-loop behavior. Trace the control flow rather than interpreting the one-millisecond argument as a Unity timing measurement.
int completed = 0; using var lifetime = new CancellationTokenSource(); lifetime.Cancel();
try { await Task.Delay(1, lifetime.Token); completed++; }
catch (OperationCanceledException) { Console.WriteLine("cancelled"); }
Console.WriteLine(completed);Unity Editor: a cached destruction token
The Unity Editor excerpt belongs to AsyncLifetimeProbe.cs, not to the console companion. Its async void Start is an engine callback with its exception boundary inside the method: expected cancellation has a separate handler, and other failures are logged. Start is not an every-enable restart hook. In a normal active run the wait completes and reports success; destroying the object first requests cancellation through the cached token. Disable-only and destruction tests must remain separate, since the active-state guard and cancellation handler explain different observable outcomes.
private async void Start()
{
CancellationToken lifetime = destroyCancellationToken;
try
{
await Awaitable.WaitForSecondsAsync(1f, lifetime);
if (this == null || !isActiveAndEnabled) return;
Debug.Log("Completed while active", this);
}
catch (OperationCanceledException) { Debug.Log("Destroyed before completion"); }
catch (Exception error) { Debug.LogException(error); }
}A guard is different from cancellation
Do not store one pooled Unity Awaitable instance and await it repeatedly as though it were an ordinary reusable Task. Obtain an operation for the intended wait and follow its documented contract. Also avoid treating a destruction-scoped token as an enable-scoped token. A feature that must cancel immediately on disable needs its own cancellation-source ownership and disposal policy, including what a later enable starts. Finally, a catch for cancellation must not swallow all unrelated exceptions under a misleading canceled label; the sample preserves a distinct diagnostic path for unexpected errors.
Two independent async lifetime experiments
First, run three independent Editor cases from a fresh Play session: leave the component active, disable it before completion, and destroy its GameObject before completion. Write down whether each case should log success, silently suppress success or take the cancellation handler. Hint: the destroy token changes only for destruction; isActiveAndEnabled is checked after a successful wait. Repeat the disable case and re-enable only after the original wait has ended. Explain why that action alone does not call Start again or produce a second scheduled wait.
Second, use a separate console copy to remove the initial Cancel call, retaining the completion increment and final counter. Predict a final value of one with no cancelled line. Then restore cancellation and add a second increment after the catch to distinguish work skipped inside the try from work performed after handling. Hint: cancellation is delivered as control flow at an observation point, not as a rule that forbids all future statements. This exercise is independent of Unity object lifetime and can be checked entirely in SharpRune.
Review completion, cancellation and observation
Review an async feature by naming the operation's owner, token source, completion thread and output guard separately. Explain what happens if completion wins the race with cancellation, rather than assuming every request guarantees the same printed message. For this lesson, the deterministic portable result remains cancelled followed by zero, while real engine observations require the Unity scene and chosen lifecycle action. Keep the exception boundary visible, cache the destruction token before destruction, and choose a separate design when a feature's lifetime should end on disable instead.
- Cache destroyCancellationToken while the component is alive.
- Distinguish cancellation from the post-await active-state guard.
- Handle exceptions inside the async void callback boundary.
- Never await the same pooled Unity Awaitable instance twice.
Unity Editor example
Set up in Unity 6
- Use Unity6.0 (6000.0); earlier Editors may not provide the same Awaitable and lifecycle API surface.
- Attach AsyncLifetimeProbe.cs to an active object and enter Play mode for the normal completion case.
- In a separate run, destroy that GameObject before the wait completes and inspect the cancellation message.
- Disable only the component in another run; note that destruction cancellation is not requested merely by disabling.
- Re-enable after the original wait has ended and confirm that Start is not a per-enable restart mechanism.
AsyncLifetimeProbe.cs
Download Unity script ↗using System;
using System.Threading;
using UnityEngine;
public sealed class AsyncLifetimeProbe : MonoBehaviour
{
private async void Start()
{
CancellationToken lifetime = destroyCancellationToken;
try
{
await Awaitable.WaitForSecondsAsync(1f, lifetime);
if (this == null || !isActiveAndEnabled) return;
Debug.Log("Completed while active", this);
}
catch (OperationCanceledException) { Debug.Log("Destroyed before completion"); }
catch (Exception error) { Debug.LogException(error); }
}
}
What to observe in Unity
Normal active completion reports success; destruction requests cancellation through the cached token. Disabling is not destruction, and Start runs once for that instance.
The console practice below isolates the C# logic. It does not run a Unity scene or emulate Unity engine callbacks.
C# companion practice
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
int completed = 0; using var lifetime = new CancellationTokenSource(); lifetime.Cancel();
try { await Task.Delay(1, lifetime.Token); completed++; }
catch (OperationCanceledException) { Console.WriteLine("cancelled"); }
Console.WriteLine(completed);
C# practice output
cancelled 0
Destruction and disabling are different lifetimes; the cached destroy token covers only the former.