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
- In your async command's Run, always return a non-nil WaitFunc closure on the success path.
- 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.
- 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
- Always return a non-nil WaitFunc from async commands on the success path.
- Beware the typed-nil-interface pitfall when returning a typed nil WaitFunc.
- Unit-test that async commands return a non-nil WaitFunc.
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
- internal error: command returned both an error and a WaitFun
- command cannot be run in background
- destination is not a directory
- no engine configured
- duplicated '!' or '?' token
AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12).
Data as JSON: /api/errors/71599a79adea723b.
Report an issue: GitHub.