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

  1. Complete tasks before completing their parent step; make step completion the final operation.
  2. Complete the step only after all child tasks reach a terminal state (await each CompleteTaskAsync).
  3. In finally/cleanup blocks, check task.CompletionState == CompletionState.InProgress before completing.
  4. 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

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


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)