Skip to content

os/exec: ExitError does not conform to errors best practice via errors.Is() #51120

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
johnnybubonic opened this issue Feb 9, 2022 · 3 comments

Comments

@johnnybubonic
Copy link

What version of Go are you using (go version)?

$ go version
go version go1.17.6 linux/amd64

Does this issue reproduce with the latest release?

Yes.

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/[REDACTED]/.cache/go-build"
GOENV="/home/[REDACTED]/.config/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/[REDACTED]/go/pkg/mod"
GONOPROXY="[REDACTED],[REDACTED]/*,github.com/[REDACTED]/*"
GONOSUMDB="[REDACTED],[REDACTED]/*,github.com/[REDACTED]/*"
GOOS="linux"
GOPATH="/opt/dev/go"
GOPRIVATE="[REDACTED],[REDACTED]/*,github.com/[REDACTED]/*"
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/lib/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/lib/go/pkg/tool/linux_amd64"
GOVCS=""
GOVERSION="go1.17.6"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/[REDACTED]/go.mod"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build1498898721=/tmp/go-build -gno-record-gcc-switches"

What did you do?

It is impossible to use errors.Is() with exec.ExitError as the result of a command execution. Consider the following:

import (
	"errors"
	"log"
	"os/exec"
)

var err error

func main() {

	var cmd *exec.Cmd

	cmd = exec.Command("somecommand", "somearg1")
	if err = cmd.Start(); err != nil {
		log.Panicln(err)
	}
	
	if err = cmd.Wait(); err != nil {
		if errors.Is(err, exec.ExitError) { // THIS WILL FAIL SYNTAX, as exec.ExitError.Error() expects a pointer receiver.
			_ = "" // Do something here.
		}
	}
	// ...
}

Because the structure field values that the *exec.ExitError contains are unpredictable, we cannot do a direct comparison by creating an exec.ExitError to compare against.

Because .Wait() (and .Run, etc.) may return errors other than *exec.ExitError, we can't simply assign the return as an *exec.ExitError (and indeed, the returned is an error interface).

This means in order to simply to determine if it's a runtime non-exec.ExitError error or a non-zero status on Linux (et. al.), one must use typeswitching or typecasting, use errors.As and check the bool of that return with an explicitly declared var for the .As(), or just simply use .Run() and hope that your command returns 0.

This is immensely cumbersome for something that oughtn't be, considering the incredible prevalence of os/exec.Command executions in many third-party libraries.

What did you expect to see?

N/A

What did you see instead?

N/A

@seankhliao
Copy link
Member

Is for ExitError wouldn't be useful as it can't convey information specific to the error.
Using As is also not much longer than Is: if e := (&os.ExitError{}); errors.As(err, &e) {}

Closing as working as intended.

@johnnybubonic
Copy link
Author

Is for ExitError wouldn't be useful as it can't convey information specific to the error.

I'd say differentiating between a golang internal error or a command returning a non-zero is a pretty useful information in and of itself:

If the command starts but does not complete successfully, the error is of type *ExitError. Other error types may be returned for other situations.

https://pkg.go.dev/os/exec#Cmd.Run

Or are we back to "typeswitching is the best practice for determining an error type" and just abandoning errors.Is being current recommendation? If I need specific logic that depends on whether that response came from the command being executed or from go itself, I am expected to then capture that with an errors.As?

That strikes me as backwards, cumbersome, and overcomplicated.

@seankhliao
Copy link
Member

Is/As serve different purposes, see https://go.dev/blog/go1.13-errors

@golang golang locked and limited conversation to collaborators Feb 9, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

3 participants