| name | async-programming |
| description | Async programming patterns with UniTask, cancellation tokens, and exception handling. Use when writing async code, handling CancellationTokenSource lifecycle, using SuppressToResultAsync, implementing detached UniTask/UniTaskVoid flows, or working with Result/EnumResult types for exception-free flow propagation. |
| user-invocable | false |
Async Programming Patterns
Sources
docs/async-programming.md — Async pattern guidelines for UniTask flows
docs/architecture-overview.md — Result struct and exception-free flow details
Core Rules
Condensed rules are in CLAUDE.md §9. This skill provides expanded patterns and code examples not covered there.
Exception Handling Pattern
Wrong — Unhandled exceptions in detached flow
async UniTaskVoid DoSomethingAsync(CancellationToken ct)
{
var result = await webRequestController.GetAsync(args, ct);
}
Correct — Proper exception handling
async UniTaskVoid DoSomethingAsync(CancellationToken ct)
{
try
{
var result = await webRequestController.GetAsync(args, ct);
ProcessResult(result);
}
catch (OperationCanceledException) { }
catch (Exception exception)
{
ReportHub.LogException(exception, ReportCategory.GENERIC_WEB_REQUEST);
}
}
Code Example — Exception Handling in Controller
From MinimapController.cs:
private async UniTask<PlacesData.PlaceInfo?> GetPlaceInfoAsync(
Vector2Int parcelPosition, CancellationToken ct, bool renewCache = false)
{
try
{
return await placesAPIService.GetPlaceAsync(parcelPosition, ct, renewCache);
}
catch (OperationCanceledException _) { }
catch (NotAPlaceException notAPlaceException)
{
ReportHub.LogWarning(ReportCategory.UNSPECIFIED,
$"Not a place requested: {notAPlaceException.Message}");
}
catch (Exception exception)
{
ReportHub.LogException(exception, ReportCategory.GENERIC_WEB_REQUEST);
}
return null;
}
SuppressToResultAsync Pattern
Wraps a UniTask<T> in a try/catch and returns a Result<T> struct instead of throwing.
Code Example — SuppressToResultAsync + Result
From TokenFileAuthenticator.cs:
private async UniTask<IWeb3Identity> LoginAsync(CancellationToken ct)
{
if (!File.Exists(TOKEN_PATH))
throw new AutoLoginTokenNotFoundException();
Result<string> contentResult = await File.ReadAllTextAsync(TOKEN_PATH, ct)!
.SuppressToResultAsync<string>(ReportCategory.AUTHENTICATION);
if (contentResult.Success == false)
throw new Exception(contentResult.ErrorMessage ?? "Cannot read token file");
string token = contentResult.Value;
}
Result and EnumResult Types
Result Struct
Zero-cost value type for exception-free flow propagation:
return Result<string>.SuccessResult(value);
return Result<string>.ErrorResult("Something went wrong");
if (result.Success)
UseValue(result.Value);
else
HandleError(result.ErrorMessage);
REnum Union Types
Roslyn Source Generator for Rust-style enums. Zero-cost pattern matching:
result.Match(
success: value => ProcessValue(value),
error: msg => LogError(msg)
);
if (result.IsSuccess(out var value))
ProcessValue(value);
Cancellation Token Management
CancellationTokenSource Lifecycle
From MinimapController.cs — proper CTS management:
private CancellationTokenSource? placesApiCts;
protected override void OnFocus()
{
placesApiCts.SafeCancelAndDispose();
placesApiCts = new CancellationTokenSource();
RefreshPlaceInfoUIAsync(previousParcelPosition, placesApiCts.Token).Forget();
}
private void OnFavoriteButtonClicked(bool value)
{
favoriteCancellationToken = favoriteCancellationToken.SafeRestart();
SetAsFavoriteAsync(favoriteCancellationToken.Token).Forget();
}
public override void Dispose()
{
placesApiCts.SafeCancelAndDispose();
disposeCts.Cancel();
favoriteCancellationToken.SafeCancelAndDispose();
}
Key patterns:
SafeCancelAndDispose() — Cancel and dispose in one call, null-safe
SafeRestart() — Cancel + dispose + create new CTS
- Always dispose CTS in
Dispose()
- Use a dedicated
disposeCts for operations that should cancel on controller disposal
Cancellation Checking
if (ct.IsCancellationRequested)
return;
ct.ThrowIfCancellationRequested();