News for hackers. Stuff that matters.
Re:Gopher Wrangling. Effective error handling in Go by janosdebugs on Tuesday June 20, 2023 @01:01AM (#36399924)
Go's error handling is a horrible mess:

1. It's easy to ignore returned errors without any compiler warnings. You have to rely on third party tools such as golangci-lint to report missing error handling.

2. Errors don't carry stack traces with them, you have to rely on third party libraries or custom errors to get that functionality and you will only get it for your own code, not in other libraries you are using.

3. It's unclear who should add context to error messages is it the caller or callee? Usually it gets skipped, leading to useless error messages.

4. Errors are untyped. If you want to decide based on error types, you have to use errors.Is or errors.As, which, surprise, is roughly as expensive computationally as panic-recover. (Source: I did a performance tests on this with Go 1.18) Go might as well add a simpler way to create exceptions. (I wrote a prototype library to that effect a while ago: https://github.com/APItalist/lang )

5. Error messages are too terse and hard to read when using the recommended semantic of "message (cause(cause(cause)))". I'd rather see stack traces, that's much more useful.

6. Most loggers are globally scoped and cannot be injected into code, leading to an all-or-nothing approach. It is not uncommon that you have 3-4 logging libraries as dependencies, which you need to configure separately (if you even can). Also, good luck securing this mess.

Re:Gopher Wrangling. Effective error handling in Go by notTooFarGone on Tuesday June 20, 2023 @01:11AM (#36399989)
Calling a linter thirdparty in Go is really disingenuous. Like you install go in your favourite IDE and it's batteries included. It's part of the standard set.
Re:Gopher Wrangling. Effective error handling in Go by aniforprez on Tuesday June 20, 2023 @03:29AM (#36400797)
golangci-lint does not come batteries included. It is a third party library. Saying it's "part of the standard set" is really disingenuous
Re:Gopher Wrangling. Effective error handling in Go by ryapric on Wednesday June 21, 2023 @01:23AM (#36414682)
I wonder if they might be talking about `go vet`?
Re:Gopher Wrangling. Effective error handling in Go by fulafel on Tuesday June 20, 2023 @05:10AM (#36401490)
Seeing the "golang" spelling as part of the project name is a sure sign something is not first party.
Re:Gopher Wrangling. Effective error handling in Go by janosdebugs on Tuesday June 20, 2023 @06:20AM (#36401964)
Go is a weird mix. It doesn't even let you create an unused variables, but happily lets you ignore errors or return variables. That makes no sense and is on Go, not on the admittedly quite excellent tooling provided by people who are not the Go dev team (third party). An IDE is just as much third party to the language as golangci-lint is.
Re:Gopher Wrangling. Effective error handling in Go by aatd86 on Tuesday June 20, 2023 @06:49AM (#36402168)
Ignoring an error is a red herring. You have to go out of your way to actually use a special character to do it.

No, one real issue that can happen if one is not careful (but fortunately linters help) is variable shadowing which may lead to some errors being unchecked.

In general, I find that error handling is not as horrible as some seem to purport.

Re:Gopher Wrangling. Effective error handling in Go by janosdebugs on Tuesday June 20, 2023 @07:10AM (#36402353)
> You have to go out of your way to actually use a special character to do it.

Only if the function returns more than the error. You can happily do this without errors:

   fh = os.Create("/some/file")
   defer fh.Close()
Needless to say, this is a terrible idea if the underlying filesystem can give you an error at close time, e.g. on NFS. The correct way to write the above code would be:

   fh = os.Create("/some/file")
   defer func() {
      if err := fh.Close(); err != nil {
          // Do something with the error
      }
   }()
Yet, I see a lot of the former and very few instances of the latter.
Re:Gopher Wrangling. Effective error handling in Go by aatd86 on Tuesday June 20, 2023 @08:28AM (#36403065)
Oh you're right. I had forgotten about that.

I think it's mostly an API legacy mistake. Close should probably return (bool, error).

Probably a remnant of coding in C wrt sentinel values.

Re:Gopher Wrangling. Effective error handling in Go by janosdebugs on Tuesday June 20, 2023 @08:56AM (#36403319)
You could still ignore both returned values. Go shouldn't allow ignoring returns without explicit dogsleds (underscore) at all if it were to stay "in character".
Re:Gopher Wrangling. Effective error handling in Go by konart on Tuesday June 20, 2023 @01:53AM (#36400234)
>3. It's unclear who should add context to error messages is it the caller or callee? Usually it gets skipped, leading to useless error messages

Why is that unclear?

Let's say you are writting a db client package and a service around it.

The package's db.Exec(query) method should return and error that will have an error text received from db if any AND\OR context from the package itself.

Then in your service you add additional context to this error if needed.

Finally you log your typical "failed to write HackerNews comment do db with err: %db_package_context: db_error_text_here%"

>6

Not sure about "most" loggers, but I have no problem with zap. Popular, definetelly can be injected etc.

Re:Gopher Wrangling. Effective error handling in Go by TheDong on Tuesday June 20, 2023 @02:10AM (#36400338)
>>3

> Why is that unclear?

The usual advice is to follow what the stdlib does. Let's look at an example. Let's say we close a file and then try to set a deadline on it:

    f, _ := os.Create("/tmp/filename")
    f.Close()
    fmt.Printf("%v", f.SetDeadline(time.Now()))

    // output: use of closed file
Okay, so in this case, it's the caller's responsibility to keep track of the filename and add the context of what file was already closed, resulting in that error.

However, what about the error for trying to write to a closed file?

    _, err := f.Write(nil)
    fmt.Printf("%v", err)    
    
    // output: write /tmp/filename: file already closed
Oh, I see, it's Write's responsibility to add the context of the filename. Huh.

This is a clear example of the problem the parent is talking about. The 'os.File' construct knows the filename. Sometimes it adds that as context to errors, sometimes it doesn't. Sometimes the caller needs to add it in, sometimes the callee has already added it.

Re:Gopher Wrangling. Effective error handling in Go by masklinn on Tuesday June 20, 2023 @04:44AM (#36401300)
> The usual advice is to follow what the stdlib does.

This seems to be a significant problem in general, because gophers want clear direction (after all the language was created specifically for… choices to be limited) so they take quips as gospels, but robpike, rsc, etc… take them more as suggestions / guidance (90/10, possibly even 80/20) to be moderated by taste.

I don’t remember which one but I think it was robpike who expressed frustration on one of the recently popular issues / proposals, because the proposal was essentially to legislate one of the more common quips, and they like being able to break those when useful or convenient.

I think there was also something similar to your exploration here with zero values, where despite the quip that you should “make the zero value meaningful” multiple standard library modules will straight up panic if fed zero values (a classic example being the File struct, `File.Name()` panics and pretty much every other method returns ErrInvalid, so a zero-valued File is useless, actively problematic, and the source of unnecessary overheads).

An other fun one is that you can’t call IsZero on a zero `reflect.Value`, and the error message is quite amazing:

    panic: call of reflect.Value.IsZero on zero Value
You need to carefully read the doc and notice that th middle paragraph documenting Value itself says:

> The zero Value represents no value. Its IsValid method returns false, its Kind method returns invalid, its String method returns “<invalid Value>”, and all other methods panic.

Re:Gopher Wrangling. Effective error handling in Go by throwaway894345 on Tuesday June 20, 2023 @08:49AM (#36403241)
I agree, the stdlib is confusing here. A safe rule is just to add the extra context in the caller unless you know the callee adds it. The worst that can happen is you include the file path multiple times and things get noisy.
Re:Gopher Wrangling. Effective error handling in Go by jjnoakes on Tuesday June 20, 2023 @05:10PM (#36410678)
I agree and I've been thinking about this in general (not only in the context of go, but for programming in general). I've settled on "caller adds context" because it is only the caller who knows if the error can be handled immediately (in which case no context is needed and we save the extra effort of creating it) or if the error should be propagated up, and how (should it be wrapped with context, wrapped in another struct, replaced with a different struct, etc).
Re:Gopher Wrangling. Effective error handling in Go by janosdebugs on Tuesday June 20, 2023 @06:08AM (#36401884)
>> 6

> Not sure about "most" loggers, but I have no problem with zap. Popular, definetelly can be injected etc.

That may be so, but a lot of libraries use a logger that isn't zap and isn't injectable, or the library doesn't expose a way to inject a logger even if the logger itself supports it. Plus, if you have 3 dependencies, you'll end up with 4 different logging libraries you need to worry about. In the end, you end up having a mess around logging unless you only rely on your own code and don't use libraries.

(Should have phrased the parent better.)

Re:Gopher Wrangling. Effective error handling in Go by konart on Wednesday June 21, 2023 @01:40AM (#36414792)
I haven't seen a package that does not provide a way to inject a logger to be honest. I'd rather not use one or an least fork one if I'm out of options.
Re:Gopher Wrangling. Effective error handling in Go by janosdebugs on Wednesday June 21, 2023 @02:00AM (#36414932)
I ran into a fair few while developing Kubernetes stuff, unfortunately. Even so, you can still end up with a bunch of different loggers in your codebase because logger libraries can't even seem to agree what log levels to support and in what form.
Re:Gopher Wrangling. Effective error handling in Go by nbraxf100 on Tuesday June 20, 2023 @03:45AM (#36400899)
The error handling is second nature to anyone who has done C or Unix programming. It just feels dirty not to check for an an error.

This is one part I like about Go.

Re:Gopher Wrangling. Effective error handling in Go by nprateem on Tuesday June 20, 2023 @04:19AM (#36401101)
Which rules out the majority of people who learnt to code in the last 25 years (many unis have taught java since 2000ish)
Re:Gopher Wrangling. Effective error handling in Go by rob74 on Tuesday June 20, 2023 @07:42AM (#36402635)
Because people don't ever learn more about programming than what they were taught in university? If this is true, I'm a bit afraid about the career perspectives of these students, and wouldn't really want to be on a team with them...
Re:Gopher Wrangling. Effective error handling in Go by nprateem on Tuesday June 20, 2023 @07:59AM (#36402801)
Only a minority will be sufficiently sadistic to learn C when better alternatives exist. So yes, the above excludes the majority.
Re:Gopher Wrangling. Effective error handling in Go by kaba0 on Tuesday June 20, 2023 @04:58AM (#36401403)
An if err with some random one-liner in the err part is not error handling.

You can’t reasonably handle an error condition on a local basis, that’s why exceptions (especially checked ones) are superior. They do the correct thing — either bubble up if it doesn’t make sense to handle them in place, or have them in as broad of a scope as it makes sense with try-catches. Oh and they store the stacktrace, so when an exception does inevitably happen in your software you will actually have a decent shot of fixing it instead of grepping for that generic error message throughout the program (especially if it’s not even written by you!). I swear people lie to themselves with all those if-errs believing they have properly handled an error condition because it took effort.

Re:Gopher Wrangling. Effective error handling in Go by arez on Tuesday June 20, 2023 @05:11AM (#36401493)
yes, that's exactly how I also think about Go's error handling. It was always praised the in the early days but it becomes more and more obvious that it's not a good way of handling errors, let alone reading code full of error returns and if err
Re:Gopher Wrangling. Effective error handling in Go by meling on Tuesday June 20, 2023 @08:02AM (#36402832)
I see this argument a lot, but error handling is also code that you probably want to read. Especially if you are debugging a problem. My experience is that exposing the error handling logic makes this easier. But I also like John Ousterhout’s suggestion to define errors out of existence where possible (A Philosophy of Software Design). But that requires more thinking than some developers like to deal with.
Re:Gopher Wrangling. Effective error handling in Go by preseinger on Tuesday June 20, 2023 @07:34AM (#36402568)
calling a function that can fail means you need to manage that condition

inspecting the err value returned by a function call is in fact error handling

the point of this design is to keep control flow "on the page"

exceptions do not keep control flow "on the page"

Re:Gopher Wrangling. Effective error handling in Go by janosdebugs on Tuesday June 20, 2023 @06:10AM (#36401895)
Unfortunately, at scale a feeling of dirtiness doesn't prevent a lot of really bad code from being written. Looking across the Go landscape, inadequate error handling is present in almost all projects I come across.
Re:Gopher Wrangling. Effective error handling in Go by citrin_ru on Tuesday June 20, 2023 @06:21AM (#36401974)
Is there a language which prevents bad error handling? Many developers praise exceptions but as an Ops person I'm sick of logs full of useless java stack traces which are hard to read and follow (even if you have source code within the reach) and despite verbosity often fail to provide the context necessary to find the failure cause. Best logs I've seen for some reason are all from apps written in C/C++. I know error handling is not only about logs, but in many cases all it does - logging and passing error up the stack.
Re:Gopher Wrangling. Effective error handling in Go by TwentyPosts on Tuesday June 20, 2023 @06:45AM (#36402127)
Rust does it pretty well. No error handling via exceptions, it's all Result<Ok, Err> or Option<Value> types.

This is great since it's enforced on a type-level that every result or option has to be unwrapped before it can be used. Unwrapping is explicit, the code panics if something goes wrong, and the function signature makes it easy to see what sort of errors to expect (especially when using custom error enums for the Err value).

There is one major caveat here, which is that Rust's type system only forces you to check for errors if you plan to use the return value of a function.

An example: You have a function write(), which writes to a file, and which returns Result<(), Err>. (Here `()` is the zero-sized type, ie. an empty tuple).

If you call this function in your code, it might return an error, but you can just silently drop this error. Your linter is going to complain, though. This issue could be fixed with proper linear types (ie. types which must be used once), but adding them to the language would afaik be really difficult at this point.

But other than that Rust is doing pretty well, honestly, and (imo) Go would be a significantly better language if it had a proper Result type, and just used it for all of its error-handling. Sadly, we can't change history.

Re:Gopher Wrangling. Effective error handling in Go by janosdebugs on Tuesday June 20, 2023 @06:52AM (#36402192)
Unfortunately, as an ops person you are always at the mercy of developers. If a dev writes this code in Go:

    if err != nil {
        return err
    }
If is equally useless as "letting if fly" in Java. However, in Go, developers seem to have more of an awareness for the need for proper error handling, which is not the case with most Java devs. So your problem is cultural, not technical.

I, for one, really like the idea of checked exceptions, forcing you to document and handle your exeptions. However, that idea has turned out to be too tedious for most people, so it didn't catch on.

Re:Gopher Wrangling. Effective error handling in Go by Tozen on Thursday June 22, 2023 @01:42PM (#36435887)
There's other opinions out there, that discuss the direction taken by Go on error handling, for example this Vlang related article[1] that touched on it (from someone who appears to be a gopher).

However, part of the issue is the tolerance or maturity necessary for being able to handle a different opinion. That someone gives a different preference, for instance Vlang or Odin (that has its own views), can make evangelists or "corporate machinery" upset. Then we can witness mob or anger downvoting. This then limits the debate or creates more of a barrier for different opinions to want to contribute or ever be seen.

[1]: "Is Vlang better than Golang in error handling?" (https://towardsdev.com/is-vlang-better-than-golang-in-error-...).

Re:Gopher Wrangling. Effective error handling in Go by amedvednikov on Tuesday June 20, 2023 @04:14AM (#36401063)
One of the main reasons I created V.

It's pretty much Go with Option/Result that forces you to handle errors:

f := os.create('foo.txt') or { println(err) return }

https://vlang.io/compare#go

Re:Gopher Wrangling. Effective error handling in Go by appleflaxen on Tuesday June 20, 2023 @06:18AM (#36401952)
It sounds like you think about error handling a lot.

Is there a language that has error handling "done well " that you like?

Re:Gopher Wrangling. Effective error handling in Go by janosdebugs on Tuesday June 20, 2023 @06:33AM (#36402040)
Error handling is a difficult topic. Generally, the more you can catch in the compiler, the less you have to write runtime checks and the obligatory unit tests that everyone likes to forget. So if you are on the lookout for a language, I'd look for something that has explicit nullable/non-nullable types, as well as strict and static typing.

However, I wouldn't pick a language purely based on its error handling capabilities. That's treating everything like a nail just because you have a hammer. I'd pick a language that's suitable for the task at hand. Go is suitable for making small(ish) webservices. Over 10k lines of code it becomes really hard to keep things straight. However, that's more due to its very limited scoping abilities.

As far as Go is concerned, you can make the error handling work. In ContainerSSH, we built our own logging overlay, which you can find here: https://github.com/ContainerSSH/libcontainerssh/tree/main/lo... This companion message library has a custom error structure that carries along an error code, which uniquely allows identifying the cause of the error: https://github.com/ContainerSSH/libcontainerssh/blob/main/me... Errors can be wrapped and we added tools to determine, if a certain error has an ancestor with a specific code, allowing for tailored error handling cases. We also added a tool that gathers the comments from the error code constants and adds them to the documentation: https://github.com/ContainerSSH/libcontainerssh/blob/main/cm...

I hope this helps.

Re:Gopher Wrangling. Effective error handling in Go by maleldil on Tuesday June 20, 2023 @09:57AM (#36404051)
> Over 10k lines of code it becomes really hard to keep things straight. However, that's more due to its very limited scoping abilities.

Could you please elaborate more on this?

Re:Gopher Wrangling. Effective error handling in Go by janosdebugs on Tuesday June 20, 2023 @11:37AM (#36405583)
With the default toolchain, Go has essentially two levels of visibility scoping: package-private (lower case) vs. exported (upper case) items, and module-internal vs. public packages (packages in the "internal" folder are not accessible from other modules). To make matters worse, all files in the same folder belong to the same package. (There are other toolchains out there that do things differently.)

In larger projects, especially with lots of contributors, I found that you'll want to practice some level of defensive programming. In other words, you'll want to make sure that parts of your program only talk to each other via "sanctioned" APIs. Go makes this difficult because anything in the same folder can access data in structures defined in the same folder.

For example, the Kubernets backend for ContainerSSH (https://github.com/ContainerSSH/libcontainerssh/tree/main/in...) has two parts, one dealing with the integration with the rest of ContainerSSH, while the other deals with Kubernetes. In order to provide some level of separation, we added an interface in the middle to document the package-internal API somewhat: https://github.com/ContainerSSH/libcontainerssh/blob/main/in... However, this is suboptimal and leads to unnecessary boilerplate code.

You could, of course, forego the interface, but that would mean that code reviewers would have to make sure nobody is taking the easy route and messing with the internal state of a struct that they have no business messing with. In other words, you'd want some level of encapsulation which is not easily doable in Go.

In other languages you have much more granular visibility options, either in an OOP way (private, protected, public, package-private, friend classes, etc), or by providing things like header files that describe an interface between parts of the application.

Re:Gopher Wrangling. Effective error handling in Go by maleldil on Tuesday June 20, 2023 @04:04PM (#36409724)
Thank you for the in-depth response! Just to clarify my understanding, do you mean you want to encapsulate data structures within the same package (i.e. files in the same directory)? If so, I'm not clear why you can't break those up into a different package.
Re:Gopher Wrangling. Effective error handling in Go by janosdebugs on Wednesday June 21, 2023 @12:23AM (#36414371)
You absolutely can, but then you a) have a TON of packages, which makes it really hard to structure in a codebase this size and b) you still can't ensure that the lower level package gets called only from the higher level package and not from somewhere else in the codebase. You could use modules, but modules have very strict versioning requirements and make large refactoring difficult. Not unsolvable problems, but difficult in a large codebase. (Go workspaces make it a little better, but not by much.)
Re:Gopher Wrangling. Effective error handling in Go by throwaway894345 on Tuesday June 20, 2023 @08:37AM (#36403130)
I agree that there is room for improvement, but I don’t mind Go’s errors that much. Using a linter to make sure errors are checked doesn’t seem like a major problem; most Go developers reflexively check errors for everything besides fmt.Println anyway.

The error interface doesn’t bother me too much either. Just use errors.Is/As to determine the type of you’re going to do something special with it. It’s way better than having to create unique error/result types for every function.

Who should add context is definitely a problem, but I’ve settled on “the callee”, but in cases where you’re calling something that doesn’t add context you will need to add it in the immediate caller.

Adding context in Go is significantly easier than in other languages (yes, you can use anyhow in Rust, but it’s not considered good practice to put this in library code), and good context largely obviates the need for stack traces anyway. Context is nicer because it can tell you, for example, which loop iteration you were in when things blew up or what the salient parameter values were—stuff you don’t get from a stack trace. Of course, you have to do a bit of work for this benefit, but fmt.Errorf makes this super easy.

Logging also irks me. You can pass a logger like any other data, but mostly people just use global loggers. I haven’t had the multiple loggers problem, but that’s because library authors in Go idiomatically do not add their own logging. What are the languages that do logging well? I’ve had a horrible time with Python (and I think Java but it’s been 10 years).

Re:Gopher Wrangling. Effective error handling in Go by simiones on Tuesday June 20, 2023 @11:18AM (#36405245)
Unfortunately, fmt.Errorf makes errors.Is/As useless. In fact, errors.Is is mostly useless in general, since very few Go libraries have any error types at all. You're usually stuck with parsing error messages if you actually want to handle errors programmatically, even for much of the standard library.
Re:Gopher Wrangling. Effective error handling in Go by philosopher1234 on Tuesday June 20, 2023 @12:28PM (#36406525)
Uh? If you use `%w` in fmt.Errorf(), it should still work with .Is and .As?
Re:Gopher Wrangling. Effective error handling in Go by pkd on Tuesday June 20, 2023 @05:42PM (#36411084)
> Uh? If you use `%w` in fmt.Errorf(), it should still work with .Is and .As?

You still need a stable error to compare against in errors.Is. fmt.Errorf is not going to provide that if the message is dynamic - which is almost always.

Re:Gopher Wrangling. Effective error handling in Go by throwaway894345 on Wednesday June 21, 2023 @02:27PM (#36422355)
I think what you're talking about with "stable" and "dynamic" are error constants (like `io.EOF`) and instances of error types (like `url.Error` https://pkg.go.dev/net/url#Error). You use `Is` for the former and `As` for the latter; fmt.Errorf's %w directive doesn't inhibit either case.
Re:Gopher Wrangling. Effective error handling in Go by simiones on Thursday June 22, 2023 @05:22AM (#36429730)
The problem is that the first-level error in most Go functions is fmt.Errorf(some error message, some args). The idea of pre-allocating an exported global error object (like io.EOF) is very rarely useful, and error structs are very rarely used either, because they require much more ceremony than just returning a fmt.Errorf() (or even an errors.New()) on the spot. But errors.Is is only useful if you take the first option, and errors.As is only useful if you take the second option.

So, in practice, neither errors.Is nor errors.As are terribly useful, and %w just gives you a false sense of usefulness.

Re:Gopher Wrangling. Effective error handling in Go by throwaway894345 on Thursday June 22, 2023 @09:56PM (#36441891)
This isn’t my experience. Most popular libraries seem to give an error type or value at least when someone might reasonably want to match on it. But yeah, if your coworkers don’t do this then Go can’t stop them (and in any case, I don’t see how fmt.Errorf() is to blame).
Re:Gopher Wrangling. Effective error handling in Go by simiones on Wednesday June 21, 2023 @04:53AM (#36416084)
If your original error is `fmt.Errorf("failed to frobulate")` or even `errors.New("failed to frobulate")`, even if it's bubbled up with `fmt.Errorf("foo() failed: %w", err)`, errors.Is and errors.As are useless for checking it (each error in the chain, including the original, will be a new instance of fmt.stringError or something like that).

This is equivalent to the problem of throwing new Exception("some message") or new RuntimeException("some message") in Java, to be fair. But this is more rarely done there in my experience, especially because people are more used to defining new exception types since the stdlib has many examples of that, unlike Go (which all returns `error` for all error types).

Re:Gopher Wrangling. Effective error handling in Go by philosopher1234 on Wednesday June 21, 2023 @01:11PM (#36421368)
Re:Gopher Wrangling. Effective error handling in Go by simiones on Thursday June 22, 2023 @05:17AM (#36429708)
That's not how people commonly use errors, with global variables.

The way code usually looks like is this:

  func foo(arg int) error {
    if arg < 0 {
      return fmt.Errorf("expected >0, got %d", arg)
    }
    if arg > 8 {
      return fmt.Errorf("expected >8, got %d", arg)
    }
    //do stuff
  }
or

  func foo(arg int) error {
    if arg < 0 {
      return errors.New(fmt.Sprintf("expected >0, got %d", arg))
    }
    if arg > 8 {
      return errors.New(fmt.Sprintf("expected >8, got %d", arg))
    }
    //do stuff
  }
In either of these cases, a caller of foo() can at best parse err.String() to see why foo() complained. But there is no way that errors.As or errors.Is help with the majority of errors returned by Go programs.
Re:Gopher Wrangling. Effective error handling in Go by Ajnasz on Thursday June 22, 2023 @08:47AM (#36431312)
I guess it's the same in most of languages. If you raise a general exception with a custom message you'll have to check the message not it's type.
Re:Gopher Wrangling. Effective error handling in Go by throwaway894345 on Thursday June 22, 2023 @09:54PM (#36441877)
That’s not the fault of fmt.Errorf(); if you aren’t giving people a type or a value to compare against there’s nothing the standard library can do to help you. You need to try string matching or maybe reflection. The standard library can’t fix bad code. Thankfully I don’t share your experience with people commonly returning those kinds of errors (at least not where one needs to match against an error).
Re:Gopher Wrangling. Effective error handling in Go by throwaway894345 on Wednesday June 21, 2023 @02:34PM (#36422452)
As others have mentioned, this isn't true. `errors.Is` is for named error instances (like `io.EOF`) while `errors.As` is for matching against a particular type of error (like `net/url.Error`). `fmt.Errorf()` doesn't inhibit either case.

https://go.dev/play/p/zsks73MGZjc