Priority events
Safety And Threading
Handle exceptions, cancellation, off-thread subscriptions, and mutation during invoke.
- Exceptions
- Cancellation
- Threading
- Re-entrancy
This page covers failure handling, cancellation, off-thread behavior, and mutation during dispatch.
Exception Handling
Each priority event has an ExceptionMode property.
CombatBus.Tick.ExceptionMode = AzExceptionMode.Isolate;ExceptionMode is applied by the InvokeSafe family. Normal Invoke and InvokeFast do not isolate handler exceptions.
Available modes:
IsolateContinue after handler exceptions. This is the default and safest general mode.
PropagateAggregateSelects the shared aggregate mode. In the current priority-event runtime, rely on
ExceptionsandFirstException; do not depend onAllExceptionsbeing populated for every priority invocation path.AbortStop on first exception. Useful for fail-fast critical flows.
SilentSelects the shared silent mode value. Current priority safe invocation still records counts and may call configured exception handlers, so do not use this as a privacy or redaction boundary.
Recommended Default
Use Isolate unless you have a specific reason not to.
CombatBus.Tick.ExceptionMode = AzExceptionMode.Isolate;Then inspect failures through InvokeSafe:
AzInvokeResult result = CombatBus.Tick.InvokeSafe();
if (result.HasExceptions)
{
Debug.LogError(result.ToDetailedString(includeStackTraces: true));
}Abort for Critical Startup
StartupBus.BeforeGameStart.ExceptionMode = AzExceptionMode.Abort;
AzInvokeResult result = StartupBus.BeforeGameStart.InvokeSafe();
result.ThrowIfFailed();Validation Result Check
ValidationBus.Validate.ExceptionMode = AzExceptionMode.PropagateAggregate;
AzInvokeResult result = ValidationBus.Validate.InvokeSafe();
if (result.HasExceptions)
{
Debug.LogError(result.ToDetailedString(includeStackTraces: true));
}AllExceptions is optional result data. For current priority events, write validation code against HasExceptions, Exceptions, and FirstException unless you have verified that the exact invocation path you use populates the aggregate list.
Global Exception Handler
AzPriorityEventConfig.GlobalExceptionHandler lets you centralize exception reporting.
AzPriorityEventConfig.GlobalExceptionHandler = (exception, eventInstance, context) =>
{
Debug.LogError($"[{context}] {exception}");
};AzPriorityEventConfig.MaxAggregateExceptions is the shared limit for aggregate exception lists when a runtime path populates them.
AzPriorityEventConfig.MaxAggregateExceptions = 32;AzPriorityEventConfig.DefaultMode exists in the shared configuration surface, but the current priority event constructors initialize ExceptionMode to AzExceptionMode.Isolate. Set ExceptionMode on each event you want to configure explicitly.
AzInvokeResult
InvokeSafe returns AzInvokeResult.
Useful members:
InvokedNumber of handlers attempted.
ExceptionsNumber of handler exceptions.
FirstExceptionFirst exception, if details are available.
AllExceptionsOptional aggregate exception list. Do not assume current priority safe invocation always populates it.
SuccessTrue when
Exceptions == 0.HasExceptionsTrue when
Exceptions > 0.IsEmptyTrue when no handlers were invoked.
SuccessRateRatio of successful handler calls.
ToDetailedString(...)Diagnostic multiline summary.
ThrowIfFailed()Throws the first available exception or a generic failure.
ThrowAggregateIfFailed(...)Throws an
AggregateExceptionif any handler failed.
Example:
AzInvokeResult result = CombatBus.HealthChanged.InvokeSafe(currentHealth);
if (result.IsEmpty)
{
Debug.LogWarning("No health listeners are registered.");
}
else if (!result.Success)
{
Debug.LogError(result.ToDetailedString(includeStackTraces: true));
}Combine multiple results:
AzInvokeResult combined =
CombatBus.Tick.InvokeSafe() +
CombatBus.HealthChanged.InvokeSafe(currentHealth);
Debug.Log(combined.ToString());Cancellation
Priority invocation methods accept optional cancellation settings:
evt.Invoke(
cancelMode: AzCancelMode.Soft,
perHandlerChecks: false,
ct: cancellationToken);AzCancelMode values:
NoneIgnore the cancellation token.
SoftExit early when cancellation is observed. Does not throw.
HardThrow
OperationCanceledExceptionwhen cancellation is observed.
By default, cancellation is checked between slots. If you pass perHandlerChecks: true, cancellation can also be checked between handlers within a slot. That is more responsive but slower.
AzCancelInfo cancelInfo;
CombatBus.Tick.Invoke(
out cancelInfo,
cancelMode: AzCancelMode.Soft,
perHandlerChecks: true,
ct: cancellationToken);
if (cancelInfo.WasCanceled)
{
Debug.Log($"Canceled at slot index {cancelInfo.SlotIndex}, handler index {cancelInfo.HandlerIndex}");
}SlotIndex is zero-based in AzCancelInfo. Slot 1 reports index 0, slot 9 reports index 8.
Off-Thread Subscription Behavior
AzPriorityEvent supports off-thread subscription and token operations by queuing operations for the main thread.
Important rules:
Invoke,InvokeFast,InvokeSlots, and other mask-based full paths should be called on the main thread.- Off-thread
Subscribequeues the subscription. - If
Subscribeis called off-thread, the returnedAzEventTokenisAzEventToken.Invalidbecause the real slot is not bound yet. - Queued operations are drained on a later main-thread invocation path.
- Token
Dispose,Pause, andResumecalled off-thread are queued.
Example:
using System.Threading.Tasks;
public sealed class WorkerSubscriptionExample
{
public void StartWorker()
{
Task.Run(() =>
{
AzEventToken token = CombatBus.Tick.Subscribe(OnWorkerRequestedTick, slot: 7);
// Off-thread Subscribe returns Invalid because the real binding is pending.
// Do not put this token in a bag.
Debug.Log(token.IsValid); // false
});
}
private void OnWorkerRequestedTick()
{
}
}For normal user code, prefer creating subscriptions on the Unity main thread. Off-thread subscription support is for advanced producer scenarios where queuing is intentional.
Off-Thread Cleanup Caveat
If you use an off-thread pending subscription and dispose the pending token before it binds, cleanup is by delegate.
That means stable method groups are much safer than throwaway lambdas:
// Better for queued cleanup:
Task.Run(() => CombatBus.Tick.Subscribe(OnTick, slot: 7));
// Harder to reason about:
Task.Run(() => CombatBus.Tick.Subscribe(() => Debug.Log("Tick"), slot: 7));Re-entrancy and Mutation During Invoke
Priority events are designed to tolerate mutation during invocation. Subscribing, pausing, resuming, or disposing during an active invoke does not corrupt the traversal.
Newly added subscriptions are not something you should depend on for immediate same-dispatch execution. Think of subscription changes as taking effect for a later stable dispatch.
private void First()
{
CombatBus.Tick.Subscribe(Second, slot: 5);
}
CombatBus.Tick.Subscribe(First, slot: 5);
CombatBus.Tick.Invoke(); // First runs. Do not rely on Second running in this same invoke.
CombatBus.Tick.Invoke(); // Second can run on a later invoke.