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: spec: add "must" operator # to check and return error #18721

Closed
jargv opened this issue Jan 19, 2017 · 47 comments
Closed

proposal: spec: add "must" operator # to check and return error #18721

jargv opened this issue Jan 19, 2017 · 47 comments
Labels
error-handling Language & library change proposals that are about error handling. FrozenDueToAge LanguageChange Suggested changes to the Go language Proposal
Milestone

Comments

@jargv
Copy link

jargv commented Jan 19, 2017

The must operator is to error handling what the go keyword is to concurrency, and the defer keyword is to resource cleanup. It turns any function into a Must*-style function with smart error handling.

NOTE: this isn't a change to the way error handling works, only to the way error handling looks.

The Must operator (#) is a unary prefix operator that can be applied to a function call. The last return value of the called function must be of type error. The must operator "peels off" the returned error from the result list, and handles that error - if it is not nil - according to the error handling strategy of the surrounding function.

There are 2 possible error handling strategies:

  1. Return zero values, plus the error
  2. Panic with the error

The error handling strategy is determined by the surrounding function's return types: if the last return value is of type error, it's error handling strategy is case 1, otherwise it's case 2.

Example:

  result := #foo(1,2,3) 

  // would be equivalent to
  result, err := foo(1,2,3)
  if err != nil {
    return 0, "", person{}, err // if the surrounding function returns an error
  }

  // -- or --

  result, err := foo(1,2,3)
  if err != nil {
    panic(err) //if the surrounding function does not return an error
  }

Some Pros:

  1. Error handling works the same way as before, but can be written a bit more tersely
  2. A function could very easily change its error handling strategy without changing its contents
  3. Adding or removing return values would no longer cascade into code that returns early because of errors. This code very often (always?) returns zero values, plus the error
  4. Example code that seeks to be terse would stop using the "drop the error" idiom:
result, _ := canFail() // silent failure!
  1. Libraries would no longer have to write "Must*" variants of functions: any function could be called with the Must Operator
  2. This is a backwards compatible change
  3. This may discourage the use of panic, as an error propagation mechanism
  4. Certain kinds of error-heavy code could have a better signal-to-noise ratio, increasing readability:
average := #getTotal() / #getCount()
  1. The programmer still has to acknowledge that an error can happen, but can use the Must operator to handle it.

Some Cons:

  1. Tools would need to be updated
  2. This may seem cryptic to new Gophers
  3. This may encouraging more use of panic by hiding the difference between panic and returning an error
  4. This may discourage the cases where errors require additional data as they propagate up the call stack:
return fmt.Errorf("while deleting user %s: %v", user, err)
// -- or --
return &CompoundError{"reason", err}

Conclusion

I believe there's precedent for the Must operator in golang. The defer and go keywords transform the way function calls work. The go keyword hides a load of complexity from the user, but always does the right thing. The defer keyword makes code less error-prone by ensuring the cleanup code will be called on every path. The Must operator is similar: it does the right thing based on the surrounding function, and it ensures that the error is handled.

@minux
Copy link
Member

minux commented Jan 19, 2017 via email

@minux minux added LanguageChange Suggested changes to the Go language Proposal labels Jan 19, 2017
@minux minux added this to the Proposal milestone Jan 19, 2017
@jargv
Copy link
Author

jargv commented Jan 19, 2017

Thanks @minux, it's good to get more background. :)

This is different from the try and when keywords proposed because it doesn't imply any kind of data binding or value watching. It avoids many of the issues in those other proposals, IMO.

@bradfitz
Copy link
Contributor

There might be some overlap here with previous proposals, but this is the most comprehensive proposal I've seen about this, and it's actually structured as a proposal, so I'm inclined to keep this open instead of closing it as a dup. Thanks.

@minux
Copy link
Member

minux commented Jan 19, 2017 via email

@jargv
Copy link
Author

jargv commented Jan 19, 2017

I agree @minux. That was one of the listed cons. I've thought about this a bit, and while I didn't add this to the proposal (to keep it simple). There could also be a binary form of the operator which calls a function, adding the error to the argument list before returning. Example:

result := UserDeleteError(user)#DeleteUser(user)

// would be equivalent to:

result, err := DeleteUser(user)
if err != nil {
  return UserDeleteError(user, err)
}

// -- or -- (depending on the error handling strategy mentioned above)

result, err := DeleteUser(user)
if err != nil {
  panic(UserDeleteError(user, err))
}

@bradfitz
Copy link
Contributor

@jargv, now you're getting into the ?: ternary operator flow control issue, with code not reading nicely top-down. And now I realize this FAQ entry kinda sucks: https://golang.org/doc/faq#Does_Go_have_a_ternary_form and doesn't provide background on the rationale.

@jargv
Copy link
Author

jargv commented Jan 19, 2017

@bradfitz I would love to learn more about that rationale, because lack of ternary is another pain point for me. :) Makes me wonder if I'm missing some of the zeitgeist of Go (though I'm still a big fan!).

Lack of ternary is especially painful when I'm creating nested struct literals and I've got to put a conditional and a variable in order to conditionally populate some field, but now the declaration of the variable and its usage are potentially quit far away, whereas the ternary allows me to elide even creating a variable. Of course, I can change from declaratively creating the struct to imperatively creating it.

Actually come to think of it, declarative struct literals feel a bit like precedent for declarative ternary and declarative error handling. Is it idiomatic to avoid struct literals in go?

@griesemer
Copy link
Contributor

griesemer commented Jan 20, 2017

For what it's worth, in the HotSpot JVM we had introduced (almost 20 years ago...) the C++ TRAPS and CHECK macros, trying to address a similar issue using the existing C++ language at that time (and trying to avoid C++ exception handling because compiler performance around functions with exception handling was unclear). The similarity with this proposal (though not notation) is striking. It's interesting to see the same ideas come up again:

Every function that could possibly "raise an exception" (cause an error) ended with the parameter TRAPS in the signature; see e.g., http://hg.openjdk.java.net/jdk7/jdk7/hotspot/file/tip/src/share/vm/prims/jvm.cpp, line 116.

Every function call that invoked such a function, ended with a last argument CHECK in the function call (see the same file, line 127).

TRAPS essentially amounted to an extra parameter passed that was used to communicate an error condition. The CHECK macro translated, well, into the following glorious macro...

THREAD); if (HAS_PENDING_EXCEPTION) return       ; (0

(http://hg.openjdk.java.net/jdk7/jdk7/hotspot/file/9b0ca45cd756/src/share/vm/utilities/exceptions.hpp, line 183).

I leave it up to the reader to decipher the macros and judge their beauty...

With respect to this proposal, I think it's actually more cryptic than the C++ approach above. Bike shedding aside, # doesn't exactly shout readability. @minux already pointed out other drawbacks. Personally, I'm not a fan for Go (despite my past life involvement with the above).

@mdempsky
Copy link
Contributor

mdempsky commented Jan 20, 2017

Instead of an operator, it could be a builtin generic function with a signature like:

func must(T1, T2, ..., Tn, error) (T1, T2, ..., Tn)

So calling it just looks like:

result := must(foo(1,2,3))

(Edit: To be clear, I'm not expressing an opinion either way on the overall proposal. I'm just suggesting a less-intrusive syntax to the originally proposed # operator.)

@jargv
Copy link
Author

jargv commented Jan 20, 2017

Ha! That's awesome @griesemer, thanks for sharing that. I've done something similar-ish in go (sans macros) to try and invert the error handling responsibility:

func foo1(errp *error, arg1, arg2 int) {
	if *errp != nil {
		return
	}
        // do something useful
}

func foo2(errp *error, arg2, arg2 string) {
	if *errp != nil {
		return
	}
        // signal failure
	*errp = fmt.Errorf("failed because reasons")
}

func main() {
	errp := new(error)
	foo1(errp, 3, 7) 	
        foo2(errp, "3", "7")
        // ...
        // fooN(errp, ...)

        // call N functions for the price of one error handling block!
	if errp != nil {
		log.Printf("*errp: %#v", *errp)
	}
}

@JayNakrani
Copy link
Contributor

JayNakrani commented Jan 20, 2017

Instead of an operator, it could be a builtin generic function

If we're having must as a builtin function, then it could also augment the error. i.e:

func fn() (..., error) {}

a := must(fn(), "extra error info")  // append/prepend string to error returned by fn

b := must(fn(), func(err error) error { /* return the "augmented" error */ })

IMO, this is much more readable than UserDeleteError(user)#DeleteUser(user).

@ucirello
Copy link
Contributor

Having written some examples in the documentation I tend to approach this proposal from the perspective of both using it in examples and impact on new gophers while reading them.

This would become, in my opinion, a very contentious operator/built-in, raising always a flamatory question on whether a such use case is more appropriate using it or not.

Frankly, I do not believe this should be a choice of the developer but of the package. If I, as a developer, must make such call, I can perfectly ending up with a divergent interpretation or decision from a fellow Gopher.

text/template and html/template are nice examples of what I am putting forward. If you happen to have a static template, that by definition shouldn't fail - and it does fail, you can use template.Must to guard against this particular failure. You can preemptively protect from it by running a test using Must and have it again as a return simplifier in the real code.

It is no coincidence we don't see Must functions spread around stdlib. I can imagine few miuses if applied to net or http.

In other words, I think this is a valid idea, but not for the language, only for specific packages that it might make sense.

@zigo101
Copy link

zigo101 commented Jan 21, 2017

how about adding a keyword, roe (return on error), which like the go and defer keyword, being used as prefixes of function callings?

@daved
Copy link

daved commented Jan 21, 2017

@mdempsky
@Dhananjay92

Instead of an operator, it could be a builtin generic function

If we're having must as a builtin function, then it could also augment the error.

This sort of minimization can obviously be performed without changing the language (https://play.golang.org/p/F-ZZySWZ5P).

v1, v2, err := valsNoErr()
v3, errx := valNoErr()
must("valsNE and valNE must", err, errx)
useful(v1, v2, v3)

must("jE must", justErr())
useful("should have bombed already")

vs

v1, v2 := must(valsNoErr(), "valsNE must")
v3 := must(valNoErr(), "valNE must")
useful(v1, v2, v3)

must(justErr(), "jE must")
useful("should have bombed already")

In my opinion, reducing a handful of lines of code is not worth introducing the incongruity of a function which can return a varying amount of values.

@JayNakrani
Copy link
Contributor

JayNakrani commented Jan 21, 2017

@ccirello, @daved

template.Must() and daved's example only work for panicing. For that case, I agree that we shouldn't change the language, because user defined functions can do it.

But most[1] of the error handling deals with augmenting the error with contextual info and propogating it back to the caller. i.e.

f, err := os.Open(p)
if err != nil {
	return fmt.Errorf("can't open file %q: %v", p, err)
}

This is the case I am more interested in. I don't see how individual packages can achieve that, without any language change.

[1]: According this visualization, most common words in Go are err, if, return, nil.

@zet4
Copy link

zet4 commented Jan 21, 2017

@golang101 that would only work if you don't care about non-error returns

@daved
Copy link

daved commented Jan 21, 2017

@Dhananjay92

This is much leaving the issue of "must". It does not require a built-in, it would require addition(s) to the error interface, if treating errors as values is not sufficient.

@xeoncross
Copy link

xeoncross commented Jan 21, 2017

My concern with "Must" would be that new developers would stop handling errors. They would just use the panic feature since it's less typing. I already see tutorials trying to do this:

x, err := foo()
checkErr(err)

Where checkErr() is just a wrapper around panic:

func checkErr(e error) {
    if err != nil {
        panic(e)
    }
}

Making it easier to not correctly handle errors seems like it would be a big detriment to the language.

@daved
Copy link

daved commented Jan 21, 2017

@xeoncross

Where checkErr() is just a wrapper around panic:

While I don't do this, I don't have much of a problem with it if the intent is to bomb out in a main func. Also, naming plays a large role as "checkErr" is unclear and even makes the example a bit of a straw-man. "errTrip", "tripOnErr", etc.; Naming can bring enough clarity and separation from regular error handling that this sort of minimization doesn't become a DRY pedant's misplaced habit. Again, I don't do this. I'm content with explicit error handling when bombing out, even if it adds a few lines.

@JayNakrani
Copy link
Contributor

@daved,

This is much leaving the issue of "must". It does not require a built-in, it would require addition(s) to the error interface, if treating errors as values is not sufficient.

Sorry, I don't understand. How can changing error interface avoid if err != nil { return ... } block? Can you provide an example, please?

@ian-kent
Copy link
Contributor

I like the idea, but share @xeoncross concerns about making it easier to not handle errors properly

That aside, if we get it, I'd prefer must func() over must(func()) or the somewhat cryptic #func() to more closely match go and defer syntax

@daved
Copy link

daved commented Jan 21, 2017

@Dhananjay92

I'm sorry, I misunderstood your statement. I had thought you were moving away from the issue of control flow operators and strictly concerned with easing the modification of errors with prefixes/suffixes. Nonetheless, your digression still takes us away from "must".

Again, in my opinion, reducing a handful of lines of code is not worth introducing the incongruity of a function which can return a varying amount of values. My suggested and currently possible approach is only good for usage of panic, as you stated, and I do not support it's use for basic error handling nor do I desire any changes to the language which would mix a relational operator, multiple control flow operators, and value modification into one.

Edit to add - It is probably worth pointing out that the only builtin which is directly related to control flow is panic.

@cstockton
Copy link

cstockton commented Jan 22, 2017

I admit when looking at word counts and scanning over large blocks of code without reading it the error checks seem repetitious. They require at least a one code block with a conditional and the assignment operation always directly precedes it. However I also believe this contributes a great deal to Go's readability, at least for me. The cognitive load of both handling and identifying operations that can fail is very minimal. I can type 3 lines of code very fast and it's so mundane I have no chance of losing any train of thought.

I haven't written any must() style functions in a long time in library code, but I do write chk() must() etc style functions in unit tests constantly and certainly find if { t.Error|Fatal() } a bit excessive at times. But I also don't need to find tests super readable, they prove your program correct once and you don't change them until you change your program. Not saying I am against this or anything, but I think it would be nice to see a entire library written / converted in the proposed style so people may get a good idea of the mental cost when reading code.

Might be worth looking at a middle ground here, just to compare against. Really this feature has two parts, the implicit checking for non-nil error interface and returning control flow to the caller with the implicit error assignment. Might be nice to see how they look individually and together, to be honest I think the below would be a nice quality of life improvement without sacrificing any readability:

  // would be equivalent to
  result, err := foo(1,2,3)
  if err != nil {
    return err // zero values are inferred when omitted for a trailing error
  }

I do find having to line-item out my zero values over and over can be a bit tedious, or dealing with values where I can just mash 0 and nil, need to stop and think about the flow of the program, defining a zero value var T or using named return params, etc. Just food for thought, nicely structured proposal though!

@dansouza
Copy link

dansouza commented Jan 22, 2017

How about introducing two keywords, fail and must ? fail as a block describes a common error handling pattern inside a function (kinda like a defer) that can be explicitly called (in which case the compiler just inlines the block in place) or implicitly called by the must() wrapper (when the called function returns a failure). must() looks like a function because it can have arguments (for error annotation mostly):

package main

import (
	"github.com/juju/errors"
)

func ParseEthernetHeader(data []byte) (mac *EthHeader, read int, err error) {
	fail (err error, reason string, args ...interface{}) {
		// first argument of a failure block is always the error
		return nil, 0, errors.Annotatef(err, reason, args...)
	}

	if len(data) < 8+6+6+3+4 {
		// one way to explicitly call the failure handler so we concentrate
		// error handling in a single place without relying on 'goto'
		fail(ErrPacketTooSmall, "packet with %d bytes too small to contain an ethernet frame", len(data))
	}
	mac = &EthHeader{}
	read += 8 // eth header preamble

	// parses a MAC address, if it fails, the fail() block is called automatically,
	// where `err` will be the error returned by ParseMAC and everything else is passed
	// as extra arguments (for annotations, etc)
	mac.Destination = must(ParseMAC(data[read:]), "invalid MAC address on destination field")
	read += 6 // source MAC
	// ...
	return mac, read, nil
}

func ParsePacket(ethernetFrame []byte) (*EthHeader, *IpHeader, *TcpHeader, *UdpHeader, []byte, error) {
	var ethernetHeader *EthHeader
	var ipHeader *IpHeader
	var tcpHeader *TcpHeader
	var udpHeader *UdpHeader
	var read int
	var totalRead int
	var payload []byte

	fail (err error) {
		// first argument of a failure block is always the error
		return nil, nil, nil, nil, []byte{}, err
	}

	ethernetHeader, read = must(ParseEthernetHeader(ethernetFrame[:]))
	totalRead += read
	ipHeader, read = must(ParseIPHeader(ethernetFrame[totalRead:]))
	totalRead += read

	switch ipHeader.Protocol {
	case 6:
		tcpHeader, read = must(ParseTcpHeader(ethernetFrame[totalRead:]))
		totalRead += read
	case 17:
		udpHeader, read = must(ParseUdpHeader(ethernetFrame[totalRead:]))
		totalRead += read
	default:
		fail(ErrProtocolUnsupported)
	}
	payload = ethernetFrame[totalRead:]
	payload = payload[:len(payload)-4]
	return ethernetHeader, ipHeader, tcpHeader, udpHeader, payload, nil
}

@AlekSi
Copy link
Contributor

AlekSi commented Jan 22, 2017

Just to note: it's not possible to add new keywords to Go 1. It will break programs where a new keyword is used as an identifier.

@christophberger
Copy link

Most of the proposals above suggest a keyword in front of the function to be wrapped with error handling. I tend to disagree that this makes a nicer look or better readability than the if err := f(); err != nil... construct that is already available in Go.

I like how Perl's ... or die idiom allows to put error handling at the end of the statement.

f() or die "f failed: $!"

(Where $! is like C's errno.)

An equivalent in Go might be something like

f() or return nil, errors.Last("f failed")   
g() or log.Println(errors.Last("f failed"))

errors.Last() would take the error value returned by f() or g() and the string argument and return a new error object, just like errors.Wrap from the pkg/errors does.

Compared to:

err := f()
if err != nil {
    return nil, errors.New("f failed: " + err)
}
if err := g(); err != nil {
    log.Println(f failed:", err)
}

this approach provides some advantages:

  • The code more concise without being cryptic.
  • The code is still readable in a top-down fashion.
  • The main control flow (i.e., the flow in the error-free case) is not disrupted by error handling statements.
  • The main flow is not wrapped inside error handling; that is, error handling does not dominate the control flow.

@dlsniper
Copy link
Contributor

Hi,

I wonder if this change should be in the language at all or not. I know that many people think that the if err != nil { return err } is too much but I could argue that in fact it makes it clear to read quickly what the code does and it's explicit about it.

We praise Go for the clarity and simplicity it brings to programming and we love it for being able to link articles that were written sometimes before 1.0 that are still relevant and apply to the language (for example: https://blog.golang.org/profiling-go-programs )

To this extent, I would propose this as change to the tooling surrounding Go rather than changing the language itself.

For example, for this example:

func test() (interface{}, error) {
	res, err := test()
	if err != nil {
		return nil, err
	}
	if res == nil {
		panic("Invalid resource")
	}
	return res, nil
}

this could look something like this:
2
or could display the actual return / panic keywords instead of the symbols show in that picture.

This way, the users that want to make this "more" readable will be able to do so, while those who don't will be able to keep the display settings as they are.

The other aspect of the proposal is already covered by most editors that support code templating as you could for example a template rerr that expand to if err != nil { return nil, err; } or perr that expands to if err != nil { panic($cursor$) } `.

One could also argue that the single line return / panic handling are not that common, depending on the software / libraries being used and that having people to actually think of error handling instead of bubbling this up is a feature not a problem. Or, as others shown, sometimes a comment is necessary to make it clear why a panic is needed instead of just returns.

As such I think this is much better suited for the tools to be change not the language and everyone gets what they need.

@dlsniper
Copy link
Contributor

As I've forgot to mention it in my previous reply, but I think others did it already. By changing the way the editors display the code but not the language we also gain the benefit of not having to change any other tool built for the language itself, adding extra knowledge and responsibilities to them in order to satisfy this.

@christophberger
Copy link

@dlsniper

A very good idea! Especially as this approach definitely does not break the Go 1 compatibility promise.

There is, however, a downside to this: Every IDE and editor plugin that implements this will implement this differently. Some of them won't implement this at all, and numerous tools (cat, less,...) will never become "Go error aware". Most web pages will continue to display plain code without any reformatting. So the same piece of code may look very different depending on the tool used to examine the code.

@dlsniper
Copy link
Contributor

There is, however, a downside to this: Every IDE and editor plugin that implements this will implement this differently.

I guess this could either be agreed or not on how the editors will choose to display this. But if you are a user of a particular editor chances are that you are rarely jumping editors on a high frequency for this to be so bad. But as I've said, editors could agree on this for the display output.

Some of them won't implement this at all, and numerous tools (cat, less,...) will never become "Go error aware". Most web pages will continue to display plain code without any reformatting. So the same piece of code may look very different depending on the tool used to examine the code.

I agree that non-Go tools and say Github / Bitbucket or other places would display this maybe in a different manner but my suggestion is that the editors that are used for Go already know they "talk" Go. It also has the advantage that it can be turned off if people don't like it, it doesn't change the language by introducing a keyword (which would be a compatibility break) as others suggested, it doesn't introduce a special syntax that would make it harder to understand what's happening.

With the current proposal the following things will happen when reading the code:

  • am I returning here?
  • am I panicking?
  • is my last argument an error?
  • move eyes over the return statement check it, move them back to where you were

This is definitely going to hinder readability, where as if an editor can just tell you in a one line that you are returning / panicking then you can move on.

We also have to approach this from a reasonable point of view: do I ever read code using cat? what will grep -rin '#demo()' tell me about the code? With the current proposal I can argue that's going to be impossible to grep for panics used as error handling as now you don't use the panic keyword anymore but a valid function call which may be either a return statement or a panic depending on the way you've wrote the function.

@jargv
Copy link
Author

jargv commented Jan 23, 2017

I was intrigued by @dlsniper's suggestion, so I started a vim plugin to try it out: jargv/vim-go-error-folds. I've only used this very briefly, but so far I kind of like it.

@dlsniper
Copy link
Contributor

To be clear, this not my idea, I've not named the source of inspiration as I'd rather keep this focused on the proposal itself rather than have a debate of merits of IDE / editors / whatnot which could be polarizing and biased.
However, for the record, the idea, screenshot as well as the discussion for it can be seen here: https://youtrack.jetbrains.com/issue/GO-3347 (it's for the upcoming Go IDE from JetBrains).
Thank you.

@christophberger
Copy link

With the current proposal the following things will happen when reading the code:

am I returning here?
am I panicking?
is my last argument an error?
move eyes over the return statement check it, move them back to where you were

The "or..." syntax does not trigger any of these questions. return and panic directives remain visible. The last argument must be an error value, otherwise the or clause would not compile.

This said, editor plugins might indeed be the way to go. After all, they are a solution that can be applied now, whereas all language changes have to wait for Go 2.

I particularly like the way how @jargv's Vim plugin turns the error handling statements into a readable and concise or syntax. Esp. when folding one-line if bodies, no information is lost nor turned into something cryptic.

To ensure the same appearance across all editors and IDE's, the Go team might want to define mandatory syntax rules for the folded expression.

@DeedleFake
Copy link

DeedleFake commented Jan 23, 2017

@xeoncross said:

My concern with "Must" would be that new developers would stop handling errors. They would just use the panic feature since it's less typing.

As an example of this exact issue, I saw this in Rust when I dabbled in it. In Rust, errors are returned as part of a tagged union called Result which can hold either the actual return value or an error. Result has a number of methods on it for handling errors, including an or() method, but most of the time people just call unwrap() which returns the return value if it's not an error or panics if it is. Large amounts of the codebases I've looked through have unwrap()s everywhere and things panic all the time as a result. Of course, calling unwrap() everywhere is discouraged, but it's so convenient that people do it all the time anyways planning to clean it up later.

As annoying and repetitive as Go error handling can get, I actually like that it forces you to handle errors when there could be one. It could certainly use some tweaking, such as to the difference between functions that just have an error return and functions that have multiple return values, but I think that whatever changes are made to it, it should always err on the annoying side rather than the convenient side.

@dlsniper
Copy link
Contributor

To ensure the same appearance across all editors and IDE's, the Go team might want to define mandatory syntax rules for the folded expression.

I think this is not needed, both from the editors and from the Go team. There's no need for the Go team to specify how code is rendered and there's no need to have this consistent across the editors either. If someone comes up along the way with a different format then everyone likes it the editors should be able to cope with having options for how to handle it.

@rsc
Copy link
Contributor

rsc commented Jan 23, 2017

I'm inclined to close this proposal but remember it for a longer error-related discussion later in the year (or just put it on hold until then). As I wrote at https://research.swtch.com/go2017#error, the big disappointment I have with the way most code uses errors is exactly that most code doesn't add important context. It justs 'return err'. Making that mistake even easier seems like a step in the wrong direction.

(And making it hard to see that whether code panics or returns an error is even worse, but that's a relatively minor point.)

@rsc rsc changed the title Proposal: The Must Operator proposal: spec: add 'must' operator to return err up Jan 23, 2017
@christophberger
Copy link

christophberger commented Jan 23, 2017 via email

@dansouza
Copy link

@rsc at the very least, I think a callstack with file:line should always be part of error types... printing an error should show a full call stack (just like a panic).

Adding extra context to errors as a key-value would be ideal too (like context.Context) - so if inserting a row into a database table fails, I can add that row itself as part of the error (and my non-stdlib logging/metrics component saves that to elasticsearch, including a JSON representation of the column).

@dlsniper
Copy link
Contributor

@christophberger lets agree to disagree. Personally I dislike the Go font so the last thing that I'd like to do is have the font option taken away because the Go team decides that that is the only way we must display the code. Same with my proposal. gofmt is a different matter altogether.

@dansouza I'd say lets hold on on this until such time is appropriate and meanwhile focus on the proposal at hand.

@christophberger
Copy link

christophberger commented Jan 23, 2017 via email

@flibustenet
Copy link

@rsc what's wrong about just returning an error when you know that this error has already a context ? (we have this problem with pkg/errors when we add the stack at each return)

@rsc
Copy link
Contributor

rsc commented Jan 23, 2017

We're going to rethink errors in Go 2, so let's leave the general topic for then. We're very unlikely to add something this substantial before then.

@thejerf
Copy link

thejerf commented Jan 23, 2017

This is not a fully-fleshed out proposal, but here's an observation on the minimal required language change: If the "must" operator 1. stripped off the err value if applicable and 2. returned the zero value of all non-err return values and passed the error through, you could get an awful lot of mileage out of:

func MayError(in Input) (out Output, err error) {
    defer func() {
        if err != nil {
            // in defer, full stack trace is still available
            err = WrapErrorWithContext(err)
        }
    }()

    out = must SomeOtherFunction()
    return
}

This pattern 1. allows wrapping return values and 2. allows must operator without a defer if the usual return mechanism is desired.

The core desire for "capturing" the error on the way out and doing something to it can be met with named parameters and defer already; in fact you can already use this technique now if you manually spell out the must with the corresponding if statement.

@sybrandy
Copy link

@rsc Is there a discussion somewhere related to errors in version 2 somewhere?

@rsc
Copy link
Contributor

rsc commented Jan 26, 2017

Not yet. I'd like to open a discussion about errors later this year.

@mh-cbon
Copy link

mh-cbon commented Feb 28, 2017

Taking this example,

err := f()
if err != nil {
    return nil, errors.New("f failed: " + err)
}
if err := g(); err != nil {
    log.Println(f failed:", err)
}

with a must keyword helper,

package main

import (
	"fmt"
	"log"
)


func f() error { return nil }
func g() error { return nil }

func work() error[, params...] {
	// the err is completely hidden
	// must [, ...vars :=] f() or mustReturn [...args]
	must f() or mustReturn
	
	// the err is copied from work via must
	// if [vars...,] err := must g() {
	must err := g() {
		return err[, vars...]
	} else {
		fmt.Println("ok")
	}
	return nil[, vars...]
}

func main() {
	
	// the err is completely hidden
	must work() or mustPanic
	
	// the err is copied from work via must
	must err := work() {
		panic(err)
	}
	
	fmt.Println("hello world")
}

Its a keyword because go language itself can t handle the requirements of such must function.

// alike signature of must
func must( [interface{}..., ]err error ) ( [interface{}..., ]error?bool? ) {
	if err != nil {
		return err
	}
	return false //|nil ?
}

Its an impossible signature,
it needs a little help from /whatever/ piece in the go language
to make something like this possibly helpful.

Also, that would surely help with the current practices
which is to manually declare must* functions.
A must keyworrd can generalize the practice
of error checking by reducing the amount
of taylor made and redundant function declaration.

The shorter versions such as must work() or mustPanic
are probably too magical to be included,
but given the predominance of error management
within any go program, i believe they worth to be explored.

As an example, error augmentation, tracing and probably more,
might be done via this func mustReturn such as
must f() or mustReturn("reason of the err: %v")

In case of multiple output arguments,
must x,y := f() or mustReturn("reason of the err x=%v y=%v: %v", x, y)

The err variable does not appear, it would be appended automatically.

Corresponding to such signature,

// alike signature of must
func mustReturn( [interface{}..., ] [format string, ] err error ) ( [interface{}..., ]error ) {
        if format=="" {
            format = "err is nto nil: %v"
        }
	return [interface{}..., ] augmentErr(format, err)
}

yet, many impossible things into this signature.

One variation of this might be,

must notFail() or 
must errors.New("some arguments") or 
must fmt.Println then 
return x, y[, err]

or, a goish version,

must x, y, err := notFail() or 
must err = errors.New("some arguments %v", err) or 
must fmt.Println(err) { 
   return x, y, err
}

or, a goish version, with a should keyword, for statements that can operate with must/or,
but does not expect an error value as a test condition requirement, behaves like a pass-through.

must x, y, err := notFail() or 
should err = errors.New("some arguments %v", err) or 
should fmt.Println(err) { 
   return x, y, err
}

The less shorter version

	must err := work() {
		panic(err)
	}

Is somehow magical, yes, but i believe
it does so, respecting as much as possible,
what would be the right way to write a go program,
without impacting user freedom.
In a few word, nice sugar syntax.

@mdempsky
Copy link
Contributor

mdempsky commented Feb 28, 2017

Its a keyword because go language itself can t handle the requirements of such must function.

Builtin functions can have any signature necessary. For example, append has signature append([]T, ...T) []T and make and new even take types rather than values as parameters. None of these are expressible as normal Go functions. Instead they're handled specially within the compiler.

We also need to continue accepting Go 1 code that uses must as an identifier.

@golang golang locked and limited conversation to collaborators Feb 28, 2018
@rsc rsc changed the title proposal: spec: add 'must' operator to return err up proposal: spec: add "must" operator # to check and return error Aug 26, 2018
@bradfitz bradfitz added the error-handling Language & library change proposals that are about error handling. label Oct 29, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
error-handling Language & library change proposals that are about error handling. FrozenDueToAge LanguageChange Suggested changes to the Go language Proposal
Projects
None yet
Development

No branches or pull requests