golang/go · critical

internal error: async command returned a nil WaitFunc

Error message

internal error: async command returned a nil WaitFunc

What it means

Internal error returned by Engine.runCommand when an async command's Run returns a nil WaitFunc AND a nil error. The script command contract requires that an async command (Usage.Async == true) return a non-nil WaitFunc so the engine can later collect its result; returning nil/nil is a contract violation by the command implementation, not a user script error.

Source

Thrown at src/cmd/internal/script/engine.go:561

	}

	return true, nil
}

func (e *Engine) runCommand(s *State, cmd *command, impl Cmd) error {
	if impl == nil {
		return cmdError(cmd, errors.New("unknown command"))
	}

	async := impl.Usage().Async
	if cmd.background && !async {
		return cmdError(cmd, errors.New("command cannot be run in background"))
	}

	wait, runErr := impl.Run(s, cmd.args...)
	if wait == nil {
		if async && runErr == nil {
			return cmdError(cmd, errors.New("internal error: async command returned a nil WaitFunc"))
		}
		return checkStatus(cmd, runErr)
	}
	if runErr != nil {
		return cmdError(cmd, errors.New("internal error: command returned both an error and a WaitFunc"))
	}

	if cmd.background {
		s.background = append(s.background, backgroundCmd{
			command: cmd,
			wait:    wait,
		})
		// Clear stdout and stderr, since they no longer correspond to the last
		// command executed.
		s.stdout = ""
		s.stderr = ""
		return nil
	}

View on GitHub (pinned to b6b368adc5)

Solutions

  1. In your async command's Run, always return a non-nil WaitFunc closure on the success path.
  2. Watch for the typed-nil-interface pitfall: `var f WaitFunc; return f, nil` returns a nil interface only if WaitFunc is referenced as such; return an explicit closure to be safe.
  3. Cover the success path with a test that asserts the returned WaitFunc is non-nil.

Example fix

// before
return func(s *script.State) (WaitFunc, error) {
    // ... start goroutine ...
    return nil, nil // async but no WaitFunc
}

// after
return func(s *script.State) (WaitFunc, error) {
    done := make(chan struct{})
    go func() { /* ... */; close(done) }()
    return func(s *script.State) (stdout, stderr string, err error) {
        <-done
        return out, errOut, nil
    }, nil
}
Defensive patterns

Strategy: validation

Validate before calling

// In a custom async command, assert the WaitFunc is non-nil before returning.
// (Inside the command's Run)
//   if wait == nil { panic("async command returned nil WaitFunc") }

Type guard

func isNilWaitFunc(err error) bool {
    var ce *script.CommandError
    return errors.As(err, &ce) && ce.Err != nil &&
        strings.Contains(ce.Err.Error(), "async command returned a nil WaitFunc")
}

Try / catch

// This is an internal/contract error; fix the offending command implementation, not the script.

Prevention

When it happens

Trigger: A registered async Cmd whose Run function returns (nil, nil). The engine's runCommand detects `wait == nil` while `async && runErr == nil` and surfaces this as an internal error to avoid a nil-pointer panic when it later calls wait(s).

Common situations: Author of a custom async script command forgets to return a WaitFunc on the success path, or returns an untyped nil in a variable typed as WaitFunc (Go nil-interface trap: `var w WaitFunc; return w, nil` is fine, but `return nil, nil` through a named return typed differently can yield a non-nil interface).

Related errors


AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12). Data as JSON: /api/errors/71599a79adea723b. Report an issue: GitHub.