golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
122.86k stars 17.52k forks source link

proposal: Go 2: error map or log for error handling #49535

Closed sergeyprokhorenko closed 2 years ago

sergeyprokhorenko commented 2 years ago

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 the error_ or error. 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 that warning("error_type") and escape("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 an if or switch statement, an error type was used that is not in the error map, then value false 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:

  1. Very concise and obvious notation even for a novice Go programmer
  2. Change is backward compatible with Go 1 (replaces, but can be used in parallel with existing error handling methods). Therefore it can be done before Go 2
  3. Simple implementation
  4. Doesn't affect compilation time and performance
  5. Explicit and composite error naming in the calling function
  6. Аrbitrary and easy error naming in the function in which the error occurred (including dynamic name generation)
  7. Ability to send errors along the chain of function calls
  8. The compiler can catch unhandled explicitly specified errors
  9. Each function can throw several errors of different types and severity, which can then be handled in different ways (including with or without instantaneous exiting the function where the error occured, with or without returning parameters)
  10. If the corresponding function is used multiple times in the same scope (that is, in visibility area), then all different types of errors will be handled correctly
  11. A desision table can be used together with an error map for error handling and informing in difficult cases

Examples of code before and after proposal

// Before proposal

package main

import (
    "errors"
    "fmt"
    "strings"
)

func capitalize(name string) (string, error) {
    if name == "" {
        return "", errors.New("no name provided")
    }
    return strings.ToTitle(name), nil
}

func main() {
    _, err := capitalize("")
    if err != nil {
        fmt.Println("Could not capitalize:", err)
        return
    }
    fmt.Println("Success!")
}

// =================================================================

// After proposal

package main

import ( // "errors" is not imported
    "fmt"
    "strings"
)

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 erroneous function. Equivalent to @capitalize["no name provided"] = true
      //escape("no name provided")   // new keyword. With escape from erroneous function
        return ""
    }
    return strings.ToTitle(name)
}

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!")
}

questionnaire.xlsx questionnaire.txt

mvdan commented 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.

sergeyprokhorenko commented 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.

Hello Daniel

I attached the required filled questionnaire to the issue

ianlancetaylor commented 2 years ago

Please attach the questions and answers as plain text, not as an xlsx file. Thanks.

ianlancetaylor commented 2 years ago

I'm sorry, I don't understand the benefit of this. What do we gain?

sergeyprokhorenko commented 2 years ago

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

seankhliao commented 2 years ago

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

sergeyprokhorenko commented 2 years ago

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.

sergeyprokhorenko commented 2 years ago

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

sergeyprokhorenko commented 2 years ago

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

sergeyprokhorenko commented 2 years ago

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!

ianlancetaylor commented 2 years ago

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.

seankhliao commented 2 years ago

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?

sergeyprokhorenko commented 2 years ago

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:

sergeyprokhorenko commented 2 years ago

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.

sergeyprokhorenko commented 2 years ago

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.

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.

DeedleFake commented 2 years ago

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.

sergeyprokhorenko commented 2 years ago

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.

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.

sergeyprokhorenko commented 2 years ago

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:

DeedleFake commented 2 years ago

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?

ianlancetaylor commented 2 years ago

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?

ianlancetaylor commented 2 years ago

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.

vatine commented 2 years ago

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"?

sergeyprokhorenko commented 2 years ago

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 commented 2 years ago

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   
}
sergeyprokhorenko commented 2 years ago

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 commented 2 years ago

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.

sergeyprokhorenko commented 2 years ago

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()

sergeyprokhorenko commented 2 years ago

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.

ianlancetaylor commented 2 years ago

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   
}
vatine commented 2 years ago

@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.

sergeyprokhorenko commented 2 years ago

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.

ianlancetaylor commented 2 years ago

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.

deanveloper commented 2 years ago

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 commented 2 years ago

@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.

sergeyprokhorenko commented 2 years ago

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:

  1. Not throw any error in the calling function
  2. Throw a new error: @calleR_function["NEW_error"] = @calleD_function["RECEIVED_error"]
  3. Escalate (forward along the chain) the received error: @calleR_function["RECEIVED_error"] = @calleD_function["RECEIVED_error"]
  4. Escalate (pass further along the chain) the received error while saving the entire path along which it was sent, for example, in the IRI format: @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.

deanveloper commented 2 years ago

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.

sergeyprokhorenko commented 2 years ago

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

sergeyprokhorenko commented 2 years ago

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"

sergeyprokhorenko commented 2 years ago

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

seebs commented 2 years ago

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".

as commented 2 years ago

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!")
}
sergeyprokhorenko commented 2 years ago

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.

sergeyprokhorenko commented 2 years ago

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.).

sergeyprokhorenko commented 2 years ago

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.

sergeyprokhorenko commented 2 years ago

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.

map[T]bool is a bit faster and convenient, but I don't insist.

sergeyprokhorenko commented 2 years ago

@gopherbot please remove label WaitingForInfo

ianlancetaylor commented 2 years ago

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.

vatine commented 2 years ago

@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 commented 2 years ago

@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.

DeedleFake commented 2 years ago

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.