microsoft/aspire · error · InvalidOperationException
Cannot complete task
Error message
Cannot complete task '{task.Id}' because its parent step '{task.StepId}' is already complete. What it means
CompleteTaskAsync enforces that the parent step is still InProgress when a task is finalized: completing a task against an already-completed step would produce incoherent reported state. The check happens under the step lock; if CompletionState != InProgress, InvalidOperationException is thrown. (The task's own idempotent-completion check runs earlier and returns silently for repeat task completion.)
Solutions
- Complete tasks before completing their parent step; make step completion the final operation.
- Complete the step only after all child tasks reach a terminal state (await each CompleteTaskAsync).
- In finally/cleanup blocks, check task.CompletionState == CompletionState.InProgress before completing.
- Catch InvalidOperationException for best-effort completion where racing completion is acceptable.
Example fix
// before await step.CompleteAsync(CompletionState.Completed, null, false); await reporter.CompleteTaskAsync(task, CompletionState.Completed, null, false); // throws // after await reporter.CompleteTaskAsync(task, CompletionState.Completed, null, false); await step.CompleteAsync(CompletionState.Completed, null, false);
Defensive patterns
Strategy: validation
Validate before calling
if (task.CompletionState == CompletionState.InProgress)
await reporter.CompleteTaskAsync(task, state, msg, false); Type guard
bool CanComplete(ReportingTask task) => task.CompletionState == CompletionState.InProgress;
Try / catch
try { await reporter.CompleteTaskAsync(task, state, msg, false); } catch (InvalidOperationException ex) when (ex.Message.Contains("already complete")) { logger.LogDebug("Step already complete; task result not recorded."); } Prevention
- Always complete child tasks before completing the step
- Await CompleteTaskAsync in the task body, not in a late finally that races step completion
- Make step completion the last operation of the step
- For best-effort cleanup, tolerate the thrown InvalidOperationException explicitly
When it happens
Trigger: Calling CompleteTaskAsync after the parent step already completed — e.g. error handlers completing the step first, then cleanup code completing its tasks; concurrent completion racing between step and task owners.
Common situations: Exception filters that complete the step on failure while the task body also completes the task in a finally block; parallel tasks completing after the step-level error path; tests completing steps eagerly.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Cannot create task for step
- Cannot update task ' ' because its parent step ' ' is…
- Parent step with ID ' ' does not exist.
- Step with ID ' ' does not exist.
- The MAUI OTLP dev tunnel configuration was not initialized…
AI-assisted analysis of microsoft/aspire@25830f84bd (2026-09-16).
Data as JSON: /api/errors/3a72a934fadb33d2.
Report an issue: GitHub.
Appendix: source
Thrown at src/Aspire.Hosting/Pipelines/PipelineActivityReporter.cs:225
public async Task CompleteTaskAsync(ReportingTask task, CompletionState completionState, string? completionMessage, bool enableMarkdown, CancellationToken cancellationToken)
{
if (!_steps.TryGetValue(task.StepId, out var parentStep))
{
throw new InvalidOperationException($"Parent step with ID '{task.StepId}' does not exist.");
}
// If the task is already in a terminal state, this is a noop (idempotent)
if (task.CompletionState != CompletionState.InProgress)
{
return;
}
lock (parentStep)
{
if (parentStep.CompletionState != CompletionState.InProgress)
{
throw new InvalidOperationException($"Cannot complete task '{task.Id}' because its parent step '{task.StepId}' is already complete.");
}
task.CompletionState = completionState;
task.CompletionMessage = completionMessage ?? string.Empty;
}
var state = new PublishingActivity
{
Type = PublishingActivityTypes.Task,
Data = new PublishingActivityData
{
Id = task.Id,
StatusText = task.StatusText,
CompletionState = ToBackchannelCompletionState(completionState),
StepId = task.StepId,
CompletionMessage = completionMessage,
EnableMarkdown = enableMarkdown
}View on GitHub (pinned to 25830f84bd)