Closed sergeyprokhorenko closed 2 years ago
Please note that you should fill https://github.com/golang/proposal/blob/master/go2-language-changes.md when proposing a language change.
Please note that you should fill https://github.com/golang/proposal/blob/master/go2-language-changes.md when proposing a language change.
Hello Daniel
I attached the required filled questionnaire to the issue
Please attach the questions and answers as plain text, not as an xlsx file. Thanks.
I'm sorry, I don't understand the benefit of this. What do we gain?
Please attach the questions and answers as plain text, not as an xlsx file. Thanks.
Hello Ian
I attached the required questions and answers as plain text to the issue
I don't understand what code would look like with this change or how it can be used. Can you please provide examples of before/after
I don't understand what code would look like with this change or how it can be used. Can you please provide examples of before/after
Hello Sean
Please do not edit my proposal, but add comments instead. I will answer your question as soon as possible.
I'm sorry, I don't understand the benefit of this. What do we gain?
Hello Ian
I inserted the required benefits of the proposal into the issue
I don't understand what code would look like with this change or how it can be used. Can you please provide examples of before/after
Hello Sean
I inserted the required examples of code before/after into the issue
Hello Dave, Sean, Hao Xin, generikvault, Vladimir, Andreas, Dominik and Gabriel
The proposal was resently substantially improved and clarified according to your questions. Therefore I hope you assess it again from scratch, change your mind and delete dislikes. Thanks!
In this code
func main() {
if @capitalize["no name provided"] { // explicit error naming in the calling function after proposal
fmt.Println("Could not capitalize: no name provided")
return
}
fmt.Println("Success!")
}
is there meant to be a call to capitalize
? All I see is a reference to @capitalize
. Does that somehow call the function? What argument does it pass?
It seems that both the caller main
and the function capitalize
have to know about the string "no name provided"
. That seems harder to use, not easier.
The first benefit you list is "Very concise and obvious notation even for a novice Go programmer" but at least for me the notation is neither concise nor obvious. I'm not saying that the current language is perfect, but I don't understand why this is better. What does warning
mean? What does escape
mean? What does @capitalize
mean? None of this is obvious.
additional questions: where does this return values? how does this handle error wrapping? how do you compare against multiple different errors? how will this integrate with existing libraries/code that returns error values?
additional questions: where does this return values? how does this handle error wrapping? how do you compare against multiple different errors? how will this integrate with existing libraries/code that returns error values?
The answers are in the proposal:
ability to send errors along the chain of function calls
is provided: a caller function can insert error codes from the called function inside its own error codes or replace them with its own error codesIf the corresponding function is used several times in the same scope, then all different types of errors will appear in the error map each time when function is used
, and the mentioned switch operator in the caller function is convenient to compare against multiple different errors (or the mentioned desision table in difficult cases)can be used in parallel with existing error handling methods
In this code
func main() { if @capitalize["no name provided"] { // explicit error naming in the calling function after proposal fmt.Println("Could not capitalize: no name provided") return } fmt.Println("Success!") }
is there meant to be a call to
capitalize
? All I see is a reference to@capitalize
. Does that somehow call the function? What argument does it pass?
No. I have demonstrated what the simplified example from the source looked like and will look like after the proposal. In this example, there was no call to the capitalize() function to get the capitalized value, since all the attention was paid to passing the error. Accordingly, I do not use the call to the capitalize() function in the program code after the sentence either.
It seems that both the caller
main
and the functioncapitalize
have to know about the string"no name provided"
. That seems harder to use, not easier.
Of course, the programmer who uses the function that caused the error must know the error codes that this function can pass. This is obvious, it is his/her usual job, and there is nothing hard about it.
So, let me see if I'm understanding this correctly: The proposal is to automatically initialize a global map for every function, and then have a new predefined function that would set string keyed booleans in that map to true in order to indicate that an error happened?
If that's correct, that seems like a massive straight downgrade to the existing system. It communicates data from function calls via global state in a similar vein to C's infamous errno
meaning that it's effectively useless for recursive functions, or even just regular function calls in any situation involving concurrency, it only allows communication of a single boolean via that state, and it loses a lot of compile-time safety due to the usage of string keyed data everywhere.
I am confused as to the intended utility of this proposal.
The first benefit you list is "Very concise and obvious notation even for a novice Go programmer" but at least for me the notation is neither concise nor obvious. I'm not saying that the current language is perfect, but I don't understand why this is better. What does
warning
mean? What doesescape
mean? What does@capitalize
mean? None of this is obvious.
This is plain English, without intricate use of interfaces, and term warning
is always used in this way: https://i.stack.imgur.com/z5Fim.png
If you don't like the term escape
, please suggest exit
, error
or anything else.
warning("error_type")
is only shortening of the operator @function_name["error_type"] = true
escape("error_type")
is only shortening of the operators @function_name["error_type"] = true; return
@capitalize[]
means error map of the function capitalize()
. This is the only thing a programmer will have to remember.
So, let me see if I'm understanding this correctly: The proposal is to automatically initialize a global map for every function, and then have a new predefined function that would set string keyed booleans in that map to true in order to indicate that an error happened?
If that's correct, that seems like a massive straight downgrade to the existing system. It communicates data from function calls via global state in a similar vein to C's infamous
errno
meaning that it's effectively useless for recursive functions, or even just regular function calls in any situation involving concurrency, it only allows communication of a single boolean via that state, and it loses a lot of compile-time safety due to the usage of string keyed data everywhere.I am confused as to the intended utility of this proposal.
No, you don't understand this proposal correctly, because:
The error map must be visible both inside the body of the function and in the scope of the declared or imported function
). There is no global statetrue
later. The error map is empty initially. The new predefined functions will add errors into the error mapThe compiler can catch unhandled explicitly specified errors
Error map is not global (The error map must be visible both inside the body of the function and in the scope of the declared or imported function). There is no global state
func Example(v int) int {
if v < 0 {
// This is at the bottom of the recursion.
// Can the original caller see it?
// Is it visible all the way up?
// Where is this map allocated?
// How is it scoped?
// What is even going on here?
escape("invalid v")
}
if v == 0 {
return v
}
return v + Example(v - 2)
}
func check() {
if @Example["invalid v"] {
panic("v was invalid") // What was the invalid value? There's no way to know.
}
}
func main() {
v := Example(29)
// Will this work? If so, that is not just global state, but global state
// that's updated on _every_ single function call. How is that going
// to work with anything concurrent? If not, what are the rules? Who
// can see the map? Are there different maps for each call? How are
// they accessed? How are they scoped?
check()
}
That sure looks like global state to me as currently proposed.
There could be many booleans for every function
I know. What I mean is that the only information that can be conveyed via this system about a given error is whether or not it happened. The only possible information is just booleans. That's not very useful.
It does not lose a lot of compile-time safety due to the usage of string keyed data everywhere, because The compiler can catch unhandled explicitly specified errors
No, it can't. What happens if I pass a string variable to warning()
instead of a constant? How is the compiler going to have any clue what its value is?
No. I have demonstrated what the simplified example from the ]source](https://www.digitalocean.com/community/tutorials/handling-errors-in-go) looked like and will look like after the proposal. In this example, there was no call to the capitalize() function to get the capitalized value, since all the attention was paid to passing the error. Accordingly, I do not use the call to the capitalize() function in the program code after the sentence either.
With respect, this statement is not correct. In the link that you cite, this is the code:
func capitalize(name string) (string, error) {
if name == "" {
return "", errors.New("no name provided")
}
return strings.ToTitle(name), nil
}
func main() {
name, err := capitalize("sammy")
if err != nil {
fmt.Println("Could not capitalize:", err)
return
}
fmt.Println("Capitalized name:", name)
}
In this example, the function main
calls capitalize
, passing the string "sammy"
.
Above, you rewrote this code using this proposal as
func main() {
if @capitalize["no name provided"] { // explicit error naming in the calling function after proposal
fmt.Println("Could not capitalize: no name provided")
return
}
fmt.Println("Success!")
}
This function no longer calls capitalize
.
So I am going to repeat my question:
Is there meant to be a call to capitalize
? All I see is a reference to @capitalize
. Does that somehow call the function? What argument does it pass?
This is plain English, without intricate use of interfaces, and term warning is always used in this way:
With respect, the term "warning" means many different things in different uses in programming. It is not always used in any one way.
It seems to me that this proposal does not simplify "find out if an error occurred" (if we can expect people to never set a false
value in the map, a len(<errormap>)
may suffice to say "no error occurred". In many cases, you have a multi-layered error handling approach, where stage 1 is "there was an error", and stage 2 is "appropriate error-specific handling". This may simplify stage 2, but certainly does not simplify stage 1.
It does not make it simpler to see if a specific error occurred. In the existing "return an error value", you could compare against package-specific sentinels, or use one of errors.Is
or errors.As
. What is does do, however, is making it impossible to return more detailed information about what failed.
It is also not at all obvious how this error propagation model works in self-recursive situations. Is there a single @function
map? Is there one that spans each self-call boundary? If the latter, how do you distinguish between the "communicates with a function I call" and "communicates with my caller"?
DeedleFake, read my answers more carefully. In them, every word is decisive (string keys, not just booleans;
explicitly specified errors, that is, constants, not variables).
Ian, I rewrote your code using my proposal:
func capitalize(name string) string { // also declares and initializes an error map @capitalize, as does the operator @capitalize := map[string]bool{}
if name == "" {
warning("no name provided") // new keyword. Without escape from errous function. Equivalent to @capitalize["no name provided"] = true
//escape("no name provided") // new keyword. With escape from errous function. In this example, the result will be the same
return ""
}
return strings.ToTitle(name)
}
func main() {
name := capitalize("sammy")
if @capitalize["no name provided"] { // explicit error naming in the calling function after proposal
fmt.Println("Could not capitalize: no name provided")
return // if it is a fatal error, otherwise remove "return"
} else {
fmt.Println("Capitalized name:", name)
}
// something else
}
With respect, the term "warning" means many different things in different uses in programming. It is not always used in any one way.
I do not insist on term "warning". As a native English speaker, you can suggest any other term with the desired meaning: pass message on minor error without exiting function
DeedleFake, read my answers more carefully. In them, every word is decisive (string keys, not just booleans; explicitly specified errors, that is, constants, not variables).
@sergeyprokhorenko, I'm pretty sure that I'm not the one misunderstanding here. I know that the keys are strings. But the keys don't carry any data. They are only identifiers. No features that you have described allow the user to pass any data besides a boolean value with your proposal. If there is a way to do it, then it should be possible to implement the following TODO
with your proposal:
func (list *List) At(i int) interface{} {
if (i < 0) || (i > list.Len()) {
// TODO: Fail and report what value i had that caused the error.
}
...
}
Also, the term 'explicitly specified' does not imply that it's a constant value, especially not in the context of a proposal with lots of implicitly declared and accessed stuff in it. If you meant that the keys had to be constants, then that only exacerbates the above problem.
vatine,
1) len(@function) is a convenient way to check for error in simple cases
2) You can pack infinitely detailed information into a string key, which can be a variable
3) Yes, there is only a single @function map in the scope of function()
DeedleFake,
String keys carry data, since not only values can be retrieved from a map, but also string keys (using the range operator). And in any case, you can check for known errors.
String keys can be both variables and constants. But the compiler can only check constants.
What would happen if I used the wrong string to index into @capitalize
? For example:
func capitalize(name string) string { // also declares and initializes an error map @capitalize, as does the operator @capitalize := map[string]bool{}
if name == "" {
warning("no name provided") // new keyword. Without escape from errous function. Equivalent to @capitalize["no name provided"] = true
//escape("no name provided") // new keyword. With escape from errous function. In this example, the result will be the same
return ""
}
return strings.ToTitle(name)
}
func main() {
name := capitalize("sammy")
if @capitalize["I accidentally wrote the wrong string here"] { // explicit error naming in the calling function after proposal
fmt.Println("Could not capitalize: no name provided")
return // if it is a fatal error, otherwise remove "return"
} else {
fmt.Println("Capitalized name:", name)
}
// something else
}
@sergeyprokhorenko
len(@function) is a convenient way to check for error in simple cases
What if we have code that looks approximately like:
if someCondition {
@function["error1 present"] = true
}
...
if otherCondition {
// If this is true, error1 is never present, none of error1, error2, or errror3 are true, due to business logic
@function["error1 present"] = false
@function["error2 present"] = false
@function["error3 present"] = false
}
This would result in the map having a non-zero length, but no true errors. Thus, len(@function)
is a sometimes-working proxy for checking all the keys. Unless we also want to introduce a boolean map, where we can never, ever assign "false". Which feels like a language change that is somewhat peculiar.
You can pack infinitely detailed information into a string key, which can be a variable
This means no longer being able to access the error status via a simple map lookup, as we no longer have the key. Instead, we'd have to loop over all the keys in the map, then check for (best case) a known error string prefix. This pretty much blows away any convenience wins your proposal has
Yes, there is only a single @function map in the scope of function()
As far as I understand this mechanism, this basically means we cannot have a situation where the same function has more than one call in the same call chain, and still be able to handle errors, as (at that point) we cannot distinguish between "errors we have set" and "errors set by a recursive call". WIth the existing returned error values, this is (essentially) trivial.
What would happen if I used the wrong string to index into
@capitalize
?
In this case the value of @capitalize["I accidentally wrote the wrong string here"]
is false
. Therefore the function fmt.Println("Capitalized name:", name)
would print Capitalized name:SAMMY
, as if you weren't wrong.
If instead of name: = capitalize ("sammy")
there were name: = capitalize ("")
, then Capitalized name:
would be printed instead of Could not capitalize: no name provided
.
Erroneous keys do not allow correct processing of data errors, but correct data will be processed correctly. Erroneous keys simply turn off data error handling. In this sense, my proposal is safe.
I believe the compiler must try to catch errors like this. This is possible if string keys are constants both in called function and in caller function.
The compiler will not be able to catch a mistake in the use of @capitalize
if the call to capitalize
is made through an interface. When calling through an interface the compiler does not know the actual method being called, and therefore does not know the set of errors that it might set. Calling through interfaces is very common in Go due to wide use of interfaces like io.Reader
.
I think the widespread confusion of people trying to review this proposal might be saying something 😅
How would this operate with multiple goroutines?
func capitalize(name string) string {
if name == "" {
warning("no name provided")
return ""
}
return strings.ToTitle(name)
}
func useridToName(uid int) string {
name := lookup(uid).Name
titleName := capitalize(name)
// data race: the error map @capitalize may be true or false depending on how
// the goroutines are scheduled.
if @capitalize["no name provided"] {
warning("user does not have name")
return ""
}
return titleName
}
func main() {
go useridToName(1)
go useridToName(2)
}
Also, a fundamental problem of this proposal seems to be that error handling is now opt-in instead of opt-out. Currently, we need to explicitly ignore errors by doing val, _ := dangerousFunction()
. However, under this proposal, it may be easy to simply not check if @dangerousFunction[...]
, not to mention that the string that we need to check in @dangerousFunction
needs to be documented in order for us to check the error. How would we simply check "did some error happen" instead of "did a specific error happen"? (just noticed the comments that mention len(@dangerousFunction)
I think if len(@dangerousFunction) > 0
is definitely a step back from if err != nil
@sergeyprokhorenko
len(@function) is a convenient way to check for error in simple cases
What if we have code that looks approximately like:
if someCondition { @function["error1 present"] = true } ... if otherCondition { // If this is true, error1 is never present, none of error1, error2, or errror3 are true, due to business logic @function["error1 present"] = false @function["error2 present"] = false @function["error3 present"] = false }
This would result in the map having a non-zero length, but no true errors. Thus,
len(@function)
is a sometimes-working proxy for checking all the keys. Unless we also want to introduce a boolean map, where we can never, ever assign "false". Which feels like a language change that is somewhat peculiar.
Since Golang does not have sets, programmers have to emulate sets using maps. Don't write code like that. Never assign false to errors.
You can pack infinitely detailed information into a string key, which can be a variable
This means no longer being able to access the error status via a simple map lookup, as we no longer have the key. Instead, we'd have to loop over all the keys in the map, then check for (best case) a known error string prefix. This pretty much blows away any convenience wins your proposal has
I do not think so. The convenience of finding an error in the error map depends entirely on how you handle errors received from the called function in the calling function.
You have many options:
@calleR_function["NEW_error"] = @calleD_function["RECEIVED_error"]
@calleR_function["RECEIVED_error"] = @calleD_function["RECEIVED_error"]
@calleR_function["error:PREVIOUSLY_calleD_function/calleD_function/calleR_function?type='RECEIVED_error'"] = @calleD_function["error:PREVIOUSLY_calleD_function/calleD_function?type='RECEIVED_error'"]
. This can be done using a shorthand syntax.Whenever possible, you should choose the simplest option that provides a convenient later search in the error map.
Since Golang does not have sets, programmers have to emulate sets using maps. Don't write code like that. Never assign false to errors.
Maps are typically emulated as map[T]struct{}
rather than map[T]bool
. Part of it is that struct{}
uses less memory, but another part is for this very reason of len(m)
correctly returning the number of items which are in the set.
The compiler will not be able to catch a mistake in the use of
@capitalize
if the call tocapitalize
is made through an interface. When calling through an interface the compiler does not know the actual method being called, and therefore does not know the set of errors that it might set. Calling through interfaces is very common in Go due to wide use of interfaces likeio.Reader
.
Ian, could you please give an example of sofware code
Yes, there is only a single @function map in the scope of function()
As far as I understand this mechanism, this basically means we cannot have a situation where the same function has more than one call in the same call chain, and still be able to handle errors, as (at that point) we cannot distinguish between "errors we have set" and "errors set by a recursive call". WIth the existing returned error values, this is (essentially) trivial.
No, you can distinguish "errors we have set" and "errors set by a recursive call" easily. Just insert count of calls into string key of error map in the IRI format: "error:function_name?type='error_type'&count=87657"
I think the widespread confusion of people trying to review this proposal might be saying something 😅
People accustomed to flaws are more likely to accept dirty hack than fundamental changes
It's not a question of willingness to accept the changes, so much as there being a lot of things in these changes that are obvious to you but not directly articulated, in ways that make it hard for anyone else to understand what you're proposing.
It appears that, at a high level, you're looking to have each function document all the errors it can throw, and allow checking them, but you're not providing a mechanism I can see for a function to do cleanup if it needs to do cleanup after an error occurs, or for things like "retry once on error".
People accustomed to flaws are more likely to accept dirty hack than fundamental changes
func main() {
if @capitalize["no name provided"] { // explicit error naming in the calling function after proposal
fmt.Println("Could not capitalize: no name provided")
return
}
fmt.Println("Success!")
}
It's not a question of willingness to accept the changes, so much as there being a lot of things in these changes that are obvious to you but not directly articulated, in ways that make it hard for anyone else to understand what you're proposing.
The point is not that I was unable to express some things that were obvious to me. But I didn't even think about these things, and community's questions help me to identify problems and find possible solutions.
But I share the idea approved by many programmers that errors are values, that is, data. I just go further in this direction, and I believe that errors should have data types and data formats. They must be stored, sent and handled as data. Specifically, errors should be detailed enough messages to allow intelligent responses.
Errors should not be references, interfaces, or any other tricky abstractions, but errors should be obvious and complete data at hand. The currently available error handling mechanism is overcomplicated, restrictive and error-prone. It should be depricated.
Of course, primitive C-style global error codes are bad. But passing errors through the last parameter of functions looks like duct tape. Errors must have their own transmission channel, not mixed with the function parameters. It is like pain impulses that propagate along special nerves separately from signals about temperature or touch. I think that for each function we need automatically generated set or (as an ersatz) map of structured error messages, available in the scope of this function. The IRI format is convenient for recording error messages. It allows to store and later add many details of any type. The serialized struct is also usable. The serialized JSON format seems overcomplicated to me, although in some cases it can be used as well.
It appears that, at a high level, you're looking to have each function document all the errors it can throw, and allow checking them, but you're not providing a mechanism I can see for a function to do cleanup if it needs to do cleanup after an error occurs, or for things like "retry once on error".
I believe that there is no need for a mechanism to forgive and forget errors. On the contrary, as in data warehouses, nothing should be forgotten or deleted. Until the destruction of the function that threw errors, all errors should be saved, provided, if necessary, with occurrence timestamps (or sequence number) and validity time intervals (in IRI format). The calling function itself should judge the relevance of errors and react its own way (cleanup, retry etc.).
How would this operate with multiple goroutines?
func capitalize(name string) string { if name == "" { warning("no name provided") return "" } return strings.ToTitle(name) } func useridToName(uid int) string { name := lookup(uid).Name titleName := capitalize(name) // data race: the error map @capitalize may be true or false depending on how // the goroutines are scheduled. if @capitalize["no name provided"] { warning("user does not have name") return "" } return titleName } func main() { go useridToName(1) go useridToName(2) }
The scope of @capitalize
error map is the same as the scope of capitalize()
function. Therefore each goroutine with the same name must have its own instance of the @capitalize
error map.
The same (many instances of the error map) goes for calls in recursion.
Maps are typically emulated as
map[T]struct{}
rather thanmap[T]bool
. Part of it is thatstruct{}
uses less memory, but another part is for this very reason oflen(m)
correctly returning the number of items which are in the set.
map[T]bool
is a bit faster and convenient, but I don't insist.
@gopherbot please remove label WaitingForInfo
The compiler will not be able to catch a mistake in the use of @capitalize if the call to capitalize is made through an interface. When calling through an interface the compiler does not know the actual method being called, and therefore does not know the set of errors that it might set. Calling through interfaces is very common in Go due to wide use of interfaces like io.Reader.
Ian, could you please give an example of sofware code
For example:
package p1
type R1 struct{}
func (r1 *R1) Read(p []byte) (int, error) { warning("no data available") }
package p2
type R2 struct{}
func (r2 *R2) Read(p []byte) (int, error) { warning("bad file") }
package p3
import "io"
// Here r can be any Reader, including R1 or R2 or some other Reader.
func ReadData(r io.Reader) {
s := make([]byte, 10)
n, err := r.Read(s)
if @Read["bad file"] { /* do something */ }
if @Read["no data available"] { /* do something else */ }
if @Read["what about all the other possible errors"] { /* how do I write this condition */ }
}
My point is that above you said
I believe the compiler must try to catch errors like this. This is possible if string keys are constants both in called function and in caller function.
That is not possible when calling a method of an interface value. When the compiler is compiling package p3 it has no idea what errors might be returned by the Read
methods in packages p1 and p2. p3 doesn't import p1 or p2.
@sergeyprokhorenko:
The scope of @capitalize error map is the same as the scope of capitalize() function. Therefore each goroutine with the same name must have its own instance of the @capitalize error map.
This would mean that the error map is not visible outside the function. Yet, it needs to be visible outside the function in order to communicate errors to callers. If it is visible outside the function, and there is exactly one per function, we have one of "goroutine-global storage" (that is, accessible to any dynamic extent within a specific goroutine) or "program-global storage". This sounds like an excellent source of synchronisation problems.
I am not, per se, opposed to another way of handling errors, I just fail to see how this proposal is:
We would also lose the ability to pass errors embedded in structs, for possible passing over a channel. Perhaps not always useful, but quite useful for the occasional "we use channels for program-internal RPCs".
@sergeyprokhorenko:
The scope of @capitalize error map is the same as the scope of capitalize() function. Therefore each goroutine with the same name must have its own instance of the @capitalize error map.
This would mean that the error map is not visible outside the function.
No, it’s wrong. The scope of anything is its visibility area. You confused the scope of the function with the body of the function, where the error map is also visible. Therefore, the scope (visibility area) of the error map is the same as the scope (visibility area) of the function parameters (including the last parameter with an error). As for the visibility area of errors, practically nothing changes in my proposal.
You confused the scope of the function with the body of the function
The term 'scope of a function' is slightly ambiguous. It usually refers to the inside of the function. The scope of its body. I think that @vatine assumed that you meant the inside of the function because if you meant the scope in which the function is visible then you didn't actually answer @deanveloper's question. If the error map is scoped to the same scope as the function definition, how does it work with concurrency? For example,
func Example(v int) int {
if v < 0 {
warning("v < 0")
}
return v + 2
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
v := Example(-1)
if @Example["v < 0"] {
fmt.Println("v < 0")
return
}
fmt.Println(v)
}()
go func() {
defer wg.Done()
v := Example(1)
if @Example["v < 0"] {
fmt.Println("v < 0")
return
}
fmt.Println(v)
}()
wg.Wait()
}
If the first call to Example()
is scheduled first, then both of these will print v < 0
. If not, only the first will. This is inherently racy.
It is also possible that you meant that the map is scoped to the same scope that a specific call happened in. If so, that fixes the concurrency issues, somewhat, but it makes it impossible to pass data back up from a recursive call without going way out of your way to do so, and doing it from a doubly-recursive call where two functions call each other back and forth recursively would be nightmarish.
But I share the idea approved by many programmers that errors are values, that is, data. I just go further in this direction, and I believe that errors should have data types and data formats. They must be stored, sent and handled as data. Specifically, errors should be detailed enough messages to allow intelligent responses.
This is a complete misunderstanding of what that means. When they say errors are values, they mean that errors should be reasoned about like any other piece of data. It's a contrast to exception-based systems where it is often considered incorrect to pass any values of an Exception
-based type around in a normal way. What you are proposing is literally the exact opposite of the intent of statement that errors are values.
New better proposal #50280 is ready
I propose the interrelated changes to the Go language:
1) Each declared or imported function (including method) must automatically and implicitly declare and initialize an error map, as does the operator
@function_name := map[string]bool{}
. Alternatively theerror_
orerror.
prefix can be used before the function name instead of the@
prefix, or the names of the function and the error map can be the same.2) The error map must be visible both inside the body of the function and in the scope (that is, in visibility area outside the function body) of the declared or imported function. The scope (that is, visibility area) of the error map is the same as scope (that is, visibility area) of the parameters of function.
But Apache Kafka attracts by the idea of a more flexible, dynamical and centralized management of the areas of visibility (topics) of messages (about errors). See description of the error log
3) The content of the error map should be updated and visible instantly, well before the called function returns, so that the calling function can decide in advance whether the called function needs to be interrupted and how to handle errors.
Cases of assigning functions to variables and transferring functions to other functions etc require special research. Instead of a map, we can use another container for error messages, if it turns out to be more convenient: set, slice, stack, etc.
Description of use:
Programmers should use error types as keys in the error map.
Each function can throw several errors of different types and severity, which can then be handled in different ways (with or without exiting the function where the error occured, with or without return of parameters). If an error occurs, then the value of its type in the error map must be
true
. Therefore, the operator@function_name["error_type"] = true
is required in the function body, but it's preferable thatwarning("error_type")
andescape("error_type")
(with escape from erroneous function) play its role.If the corresponding function is used several times in the same scope (that is, in visibility area), then all different types of errors will appear in the error map each time when function is used.
If, when checking the expression
@function_name["error_type"]
in anif
orswitch
statement, an error type was used that is not in the error map, then valuefalse
will be returned. It is convenient and obvious. A desision table can be used together with an error map for error handling and informing in difficult cases.Benefits of the proposal:
Examples of code before and after proposal
questionnaire.xlsx questionnaire.txt