Skip to content
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

proposal: promote panic(nil) to non-nil panic value #25448

Open
bradfitz opened this issue May 17, 2018 · 20 comments

Comments

Projects
None yet
@bradfitz
Copy link
Member

commented May 17, 2018

Calling panic with a nil panic value is allowed in Go 1, but weird.

Almost all code checks for panics with:

     defer func() {
        if e := recover(); e != nil { ... }
     }()

... which is not correct in the case of panic(nil).

The proper way is more like:

     panicked := true
     defer func() {
        if panicked {
              e := recover()
              ...
        }
     }()
     ...
     panicked = false
     return
     ....
     panicked = false
     return

Proposal: make the runtime panic function promote its panic value from nil to something like a runtime.NilPanic global value of private, unassignable type:

package runtime

type nilPanic struct{}

// NilPanic is the value returned by recover when code panics with a nil value.
var NilPanic nilPanic

Probably Go2.

@gopherbot gopherbot added this to the Proposal milestone May 17, 2018

@gopherbot gopherbot added the Proposal label May 17, 2018

@bradfitz bradfitz added LanguageChange and removed Proposal labels May 17, 2018

@gopherbot gopherbot added the Proposal label May 17, 2018

@cznic

This comment has been minimized.

Copy link
Contributor

commented May 17, 2018

Dear runtime,

if I perform panic(nil) or panic(somethingThatCanBeNil), it may be a mistake. That's my problem. But I may also do that intentionally and with the magically changed value, I need to not forget about that and workaround the magic.

Less the magic, the less exceptional rules I have to think about. It makes me more productive and my code more comprehensible to my future self. Thanks.

edit: typo

@mdempsky

This comment has been minimized.

Copy link
Member

commented May 17, 2018

An alternative solution would be to allow recover() to be used in ,ok assignments. For that to really solve the stated problem though, that would require all call sites to be updated.

My personal leaning at the moment is in favor of the proposal as stated.

@mvdan

This comment has been minimized.

Copy link
Member

commented May 17, 2018

Is any program out there using nil panics intentionally? A simple search for panic(nil) doesn't give anything on my entire GOPATH besides a go/ssa/interp test. But I'm more worried about panics with variables that could/would be nil.

In any case, I agree with the sentiment and the proposed solution. Perhaps runtime.NilPanic should be clarified that it's only for untyped nils, though. For example, this case has a nil value but wouldn't be equal to nil when recovered:

var err *myError
panic(err)
@cznic

This comment has been minimized.

Copy link
Contributor

commented May 17, 2018

FTR: I admit this is a real problem. I just prefer explicitly handling it.

@robpike

This comment has been minimized.

Copy link
Contributor

commented May 18, 2018

Is this a real problem, though? I doubt it. What if the implementation of panic with a nil argument instead just did, panic("nil value passed to panic")? Thereby fixing the problem and diagnosing it one one stroke.

@bradfitz

This comment has been minimized.

Copy link
Member Author

commented May 18, 2018

Is this a real problem, though? I doubt it.

I've had to deal with it at least twice. net/http had hangs when people panicked with nil.

@mpvl was talking about error handling the other day and was showing some examples of how defensive code should ideally look like (and how it's hard to get right), and he forgot the nil panic case, showing it's even harder than it seems.

What if the implementation of panic with a nil argument instead just did, panic("nil value passed to panic")? Thereby fixing the problem and diagnosing it one one stroke.

That's what I'm proposing, except with a variable (which could have a String method with that text). But I'm fine (but less fine) with it being that string value exactly, as matching on strings is a little gross.

@rsc rsc added the Go2 label May 21, 2018

@wgoodall01

This comment has been minimized.

Copy link

commented May 24, 2018

This proposal makes total sense--for a language which prides itself on its simplicity and obviousness, it is perplexing that the only way to check for a panic(nil) in a recover is by using some other variable. I can't possibly think up a situation when someone would call panic(nil) on purpose anyhow.

@deanveloper

This comment has been minimized.

Copy link

commented May 30, 2018

While I believe that this is a very good thing to be able to handle, I think that a , ok pattern would be a bit better. If I panic(nil) then in a language like Go, I would want nil to be the recover() value.

So then recover would look like:

defer func() {
    if e, ok := recover(); ok {
        // do recovery stuff
    }
}();
@cznic

This comment has been minimized.

Copy link
Contributor

commented May 30, 2018

@deanveloper I like your idea for being backwards compatible.

@bcmills

This comment has been minimized.

Copy link
Member

commented Sep 10, 2018

The proper way is more like: […]

That turns out not to be correct either: runtime.Goexit can terminate the goroutine without a recoverable value, but that pattern (and the variant as written by @cznic) would incorrectly report a panic where no panic occurred.
(https://play.golang.org/p/yS1A1c5csrR)

As far as I can tell, there is no way for a defer call to distinguish between panic(nil) and runtime.Goexit(), which to me suggests that at least one of them should be removed.

@bcmills

This comment has been minimized.

Copy link
Member

commented Sep 10, 2018

As far as I can tell, there is no way for a defer call to distinguish between panic(nil) and runtime.Goexit(), which to me suggests that at least one of them should be removed.

As it turns out, we can use the fact that a runtime.Goexit() cannot be recover'd to distinguish it from panic(nil), via a “double defer sandwich”:
https://play.golang.org/p/nlYYWPRO720

@gopherbot

This comment has been minimized.

Copy link

commented Sep 10, 2018

Change https://golang.org/cl/134395 mentions this issue: errgroup: rethink concurrency patterns

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

commented Sep 12, 2018

I think we should treat panic(nil) as a runtime error, so it should panic with a value that implements the runtime.Error interface. This is less convenient for people who want to explicitly detect panic(nil), but I don't see why that matters; if you want to check for it, then, instead, don't do it.

@deanveloper

This comment has been minimized.

Copy link

commented Sep 12, 2018

if you want to check for it, then, instead, don't do it.

Perhaps you're using a badly-written library, and you don't have control over it. I've seen worse before 🤷‍♂

@dbuteyn

This comment has been minimized.

Copy link

commented Apr 3, 2019

Our team spent 3 hours today hunting down what we now know to be a panic(nil) call. The problem is that panic(nil) is valid yet undetectable since recover() only returns the value that was panic-ed.

This proposal as-is would work as the common if recover() != nil { pattern would see a non-nil value. Therefore, has my vote. (Some internal type would work too, as long as it is not nil)

Making recover() return two values (the panic-ed value and whether a panic occurred) as mentioned several times would make more sense, However, changing recover() will undoubtedly be rejected for being non-backward compatible. Or too complicated if made magical (like map and range that provide one or two values, depending how you use it).

@go101

This comment has been minimized.

Copy link

commented Apr 3, 2019

Sometimes. people may call panic(nil) for success. See the case 3 in this article for an example.

[edit] in this example, the panic value is not nil, but it can be.

@bradfitz

This comment has been minimized.

Copy link
Member Author

commented Apr 3, 2019

@go101, eliminating code like that would be an added bonus.

@go101

This comment was marked as off-topic.

Copy link

commented Apr 4, 2019

I agree this pattern is some weird. But I think panic(nil) should be treated as an unexpected feature.

Another use case (also some weird, ;D):

func doSomething() (err error) {
	defer func() {
		err = recover()
	}()
	
	doStep1()
	doStep2()
	doStep3()
	doStep4()
	doStep5()
	
	return
}


// panic with nil for success and no needs to continue.
// panic with error for failure and no needs to contine.
// not panic to continue.
func doStepN() {...}

which is much less verbose than

func doSomething() (err error) {
	shouldContinue, err := doStep1()
	if !shouldContinue {
		return err
	}
	shouldContinue, err = doStep2()
	if !shouldContinue {
		return err
	}
	shouldContinue, err = doStep3()
	if !shouldContinue {
		return err
	}
	shouldContinue, err = doStep4()
	if !shouldContinue {
		return err
	}
	shouldContinue, err = doStep5()
	if !shouldContinue {
		return err
	}
	
	return
}

// if err is not nil, then shouldContinue must be true.
func doStepN() (shouldContinue bool, err error) {...}
@bradfitz

This comment was marked as off-topic.

Copy link
Member Author

commented Apr 4, 2019

@go101, this is quickly going off topic. All that code might be amusing to some, but it's not code we want to enable or encourage.

@go101

This comment was marked as off-topic.

Copy link

commented Apr 4, 2019

I agree.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.