hashicorp/terraform · error

operation canceled

Error message

operation canceled

What it means

Returned from `Meta.runOperation` (the operation runner in `meta.go`) when the user sends a **second** interrupt signal (e.g. a second Ctrl+C) after a graceful `Stop()` was already requested. The first signal triggers graceful stop; the second escalates to `FatalInterrupt()` and `op.Cancel()`, then this error is returned after at most a 5-second grace window for the operation to drain.

Solutions

  1. Send the interrupt only once and wait for graceful stop; the operation should finish or print its own interrupt diagnostics.
  2. If the operation genuinely hangs past the 5s grace window, file a bug against the provider that is not honoring context cancellation.
  3. In automation, send a single SIGTERM/SIGINT and rely on Terraform's own graceful shutdown rather than escalating.
Defensive patterns

Strategy: try-catch

Type guard

// Distinguish a hard cancel from a normal operation result:
func isHardCancel(err error) bool {
    return err != nil && err.Error() == "operation canceled"
}

Try / catch

op, err := meta.runOperation(...)
if err != nil && err.Error() == "operation canceled" {
    // user sent a second SIGINT; exit without treating as a Terraform failure
    return exitCodeFromFatalInterrupt()
}

Prevention

When it happens

Trigger: Two signals arrive on `m.ShutdownCh` before `op.Done()` fires: the inner `select` catches the second `<-m.ShutdownCh`, calls `FatalInterrupt` + `Cancel`, waits up to 5s for `op.Done()`, and returns `errors.New("operation canceled")`.

Common situations: User impatiently hits Ctrl+C twice during `terraform apply` / `plan` / `destroy`; long-running provider operations that do not respond quickly to the first interrupt; SIGINT delivered by orchestration that double-sends.

Related errors


AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11). Data as JSON: /api/errors/0448f3fdae487320. Report an issue: GitHub.

Appendix: source

Thrown at internal/command/meta.go:530

		// Notify the user
		opReq.View.Interrupted()

		// Still get the result, since there is still one
		select {
		case <-m.ShutdownCh:
			opReq.View.FatalInterrupt()

			// cancel the operation completely
			op.Cancel()

			// the operation should return asap
			// but timeout just in case
			select {
			case <-op.Done():
			case <-time.After(5 * time.Second):
			}

			return nil, errors.New("operation canceled")

		case <-op.Done():
			// operation completed after Stop
		}
	case <-op.Done():
		// operation completed normally
	}

	return op, nil
}

// contextOpts returns the options to use to initialize a Terraform
// context with the settings from this Meta.
func (m *Meta) contextOpts() (*terraform.ContextOpts, error) {
	workspace, err := m.Workspace()
	if err != nil {
		return nil, err
	}

View on GitHub (pinned to d32a084675)