Hacker News new | past | comments | ask | show | jobs | submit login
[flagged] Go's Error Handling Is Perfect (verygoodsoftwarenotvirus.ru)
58 points by gus_leonel 10 months ago | hide | past | favorite | 111 comments



My main issue with errors in go (last time I used it, which was a while ago) is not any of the things mentioned, it's that the default pattern you learn provides no stack trace whatsoever in the console.

There's no error handling in the python code, yet the stack trace will lead you to exactly to where you need to look. To me that is strictly superior when it comes time to debug an error.

Edit: maybe not strictly superior, as has been mentioned in replies, wrapped errors with good context at each step does seem like it would be very high SNR.


This, and most of the time if you’re simply bubbling up an error it’s busy work. Languages like python get out of your way here. If python was able to reliably tell you possible exceptions (within reason - at a certain level generic errors like out of memory can be ignored since they’re globally present) and provide a convenient way to handle them that’d be great.

This is domain dependent, but most of the time errors are handled by “catch it, log it, display an oops and move on”, and optimizing for that case is very pragmatic.


> If python was able to reliably tell you possible exceptions

I know typed errors in Java are regarded by many as a failure... but it is still conceptually very appealing to me.

I love result types and pattern matching in more typed/functional languages, except that they often have similar issues to go re: not having a stack trace.


I liked checked exceptions, but higher-order functions break them. The method signature of stream.map(f) allows a generic type for f but it can't vary with f's exceptions. The choices usually boil down to "only throw unchecked exceptions" (like Runnable) or "throw any exception" (like Callable).


It’s worth noting that this is specifically a shortcoming of Java’s type system and not an unsolvable problem. I’m a fan of Swift’s solution where you can directly mark a function as only rethrowing errors from a given parameter; it’s a lot simpler than making the error type a full fledged type parameter and covers most scenarios.


Stack traces and error handling are different thing. You want a stack when you have an unhandled error and need to figure out what went wrong. By definition "handling" an error goes down some known path where you know where you were and why. I mean, it's true you can handle errors well or badly; but as a general rule if you need a stack to figure out what your handling output means, you did it badly.


It is quite a hard thing to predict which errors will be handled and which will not at the point that you create an error :)

So I disagree. This is a pedantically narrow view of what "error handling" is. Printing a stack trace and crashing is also "handling" it.


> It is quite a hard thing to predict which errors will be handled and which will not at the point that you create an error :)

Not really, no? To first approximation all errors are system call failures: file not found, OS couldn't hand you enough memory so you got a heap allocation failure, network timeout, etc... You know exactly where those can happen because they are exposed as functions with some kind of error code specified as part of the return value.

There are situations where that's not true, but those situations tend to be fairly rare: things like "schema" violations in some backend data store ("product has a null manufacturer field", which should never happen but it turns out it does for data imported via that merger 2 years ago). And these tend to have correspondingly complicated handling ("add it to the queue of manual fixup requests") not well captured by "print a stack trace and crash".


> Not really, no? To first approximation all errors are system call failures

How does this help predict whether they are handled or not higher up the stack?


Again, demanding the need to know what is happening "higher up the stack" to understand an error is a symptom that the error is UNhandled. Handling it means understanding those things, generally by inspecting the return code in the higher caller where the context is understood.

And you're absolutely right, if you don't handle errors than a stack trace is a great way to reverse engineer the context. But that's not "handling" the error, it's debugging. And those are different tasks.


So to be clear, are you saying we don't need stack traces because we should handle 100% of errors instead?


I genuinely don't understand how you get from the sentence "You want a stack when you have an unhandled error and need to figure out what went wrong." to "You are saying we don't need stack traces". All I can suggest is that you re-read the thread more carefully.


It's generally good practice to not just `return err`, but to `return fmt.Errorf("some context: %w", err)`. That's usually enough for you to get what you need, with a much higher signal-to-noise ratio than a stack trace.


Correct me if I'm wrong but that still won't have file names and line numbers, right? So not only do you have to remember to wrap correctly, you need to take care to keep "some context: " a decently greppable string.

I agree that wrapping errors that way is better than not, but I still find it a bit lacking.


A (sad?) workaround I've seen is to intentionally misspell words in error / log messages so that they're unique enough. I guess I don't blame people doing that, but it does point to limitations of the language.


I've started to add types to log messages so they can be easily sorted with a nice GUI. Your way works too.


This, and sooo many other shortcomings, begins to explain why a submission extolling Go's error handling is already flagged.

Go, and it's error handling (and for that matter it ludicrously verbose failure to bubble up errors) is a steaming pile of shit.


That's correct. The resulting error message won't be a stack trace, but I find it spells out the problem quite nicely in practice. I haven't had issues with grepping for the errors, but I can see that it would be a problem in larger codebases.


Easy way around this, when you create the error message always put in there the local epoch time of when you are writing it (copy from date, plug-in in your editor, whatever) making grepping for it trivial later on.


That still does not let you know how you ended up at the error site. There could be multiple paths, and this approach does not help you determine that. Stack traces are still more useful.


i print the error along with file and line number every time i return it. clunky, but it works.

in fact i print file and line with every log message.

https://github.com/nathants/libaws/blob/87fb45b4cae20abd1bb1...


It's not a good practice to print stack trace anyway, you have a network timeout, you don't need 20 lines of stacktraces.

Most logger in Go can it though.


How do you identify which network call is the one that timed out?


You wrap errors with fmt.Errorf(). I work on Go daily and I don't need a stacktrace to tell me where the problem is, the error is self explanatory.


Your errors have messages, most of the time your "did not connect" error gets caught inside your "DiscountService" And returns a "could not connect to discount service error" ...

Its funny that were having this conversation because "discount service" is a thing in my life. Its sporadic failures means I just re-try immediately and get a success... Errors are part of flow control.


> It's not a good practice to print stack trace anyway

What makes it not a good practice?

> you don't need 20 lines of stacktraces

I don't _need_ stacktraces, but they are useful to me.


why would you need a stack trace at a panic? You know where the panic is already. 9/10 Go tutorials have you either panic, or write a message to the console. So write a more meaningful message so you know where in the code it's coming from. Stack traces requires instrumentation which can be stripped. I'd prefer not to ship my debug symbols.


All of that sounds like more work then what you get for 0 effort with a stack trace.


runtime/debug PrintStack() is 0 effort… what are you going on about? Stack traces are not required, only preferred by some because they don’t program defensively. If you want to include debug information in your binaries, go for it - I’ll be waiting with Ghidra


In other languages I don't have to explicitly print the stack. So I would not call that 0 effort.

I do not care about hiding my debug symbols. Anything I write that has a binary you can touch, you can just go read the source directly.


Not everything is open source... I get where you are coming from but there's a lot of software out there, that is written in go, that isn't and they want to keep it that way as much as possible.

   go run main.go
Using the go debugger in your IDE will blow up when that part of the code that panics on error is hit. Do you still need a stack trace then? You're literally looking at it in your editor. There's a time and a place for stack traces in my view and I enjoy the fact that in Go, it's not on by default.

If you're building a cloud service in Go and you're unsure of your branching paths, by all means, use stack traces and eliminate your own errors.


I think it's fallen out of favor, but I always preferred the "Either" monad style of error handling. I think the cool kids call it "railway oriented programming" but I never liked that term. Regardless, it's not radically different than the Go style, just instead of returning back two things and checking for `nil`, it's wrapped up into an algebraic data type.

I like this better, because you have the option of very easily doing the "Go style" of checking for success or failure if you want, but you can also exploit the fact that it's a monad and a functor and thus you can just use map and/or bind for all the downstream functions, so you don't have to explicitly check if you don't want to.

I feel like this is the best of both world; it still forces you to be explicit (as it's compile-time enforced by the type system), but it gives you nice syntax to make the code cleaner.


> I think it's fallen out of favor, but I always preferred the "Either" monad style of error handling. I think the cool kids call it "railway oriented programming" but I never liked that term. Regardless, it's not radically different than the Go style, just instead of returning back two things and checking for `nil`, it's wrapped up into an algebraic data type.

Is that falling out of style? Sounds like Rust's Result type, but i'm unfamiliar with monads/etc.

If it is similar, i spent ~5 years in Go and have now had about equal time in Rust - i vastly prefer those algebraic types for errors. Notably because it _can_ be as simple as Go's string concat behavior, but it can also be far more expressive. Yes, Go's type can also be more expressive by adding interfaces/etc, but it's difficult to beat a good enum in my experience.

At risk of sounding Rust biased, proper Enums are something that would have made Go more elegant imo. Unlike Generics/etc which are a contentious topic (or used to be, at least), proper enums are just so damn handy and cost so little. My 2c.


> Is that falling out of style?

I'm a bit outta the loop in the pure FP world now, because I've been stuck doing Java and C# for the last 5 or so years, but I think that world is trying to move to an algebraic effects-style system. People keep trying to explain to me why that's "better" but I'm not really sold on it, because it just feels like we're reinventing goto, though generally when I have contrary opinions like that it turns out that I'm wrong.

I don't really know Rust very well, but I suspect it has a similar way of doing `map` and `bind` to handle downstream stuff, since it has functional roots. The biggest issue I ever had with it in F# is the fact that they don't really have a concept of a monad transformer and therefore you can't easily combine monads, so if you have to mix `async` and the `Choice` monads (more or less F#'s version of Either), you have to explicitly craft your own custom monad. It's really not that bad but it's less sexy than proper monad transformers.

Algebraic data types are so utterly useful that I get annoyed that it's not standard in every language.


I think it's only academic FP that's currently interested in effect systems. most production FP still use the traditional monad error handling.

Rust's `Result` type does indeed have a `.map()` function, which works how you'd expect (and `Result`is an iterator, which give syntax for it).

Yeah, algebraic data types are very nice, and feel like such a free lunch. I cry every time i have to implement something that should have been a one-line sumtype definition as a class hierarchy.


Sadly, I think that is indeed radically different from Go’s design. Go lacks anything like sum types, and proposals to add them to the language have revealed deep issues that have stalled any development. See https://github.com/golang/go/issues/57644


I think it's still similar in the sense that the errors are encapsulated explicitly as part of the "return" value. Go gets around the lack of Sum types by requiring you to return two values, but if you wanted to do it the Go style in something like F# you're certainly able to:

    let res = riskFunc "input"
    match res with
    | Choice1Of2 result -> // successfulPath
    | Choice2Of2 err -> // Error Handling!!!
That's more or less what I meant by "not radically different". The implementation is of course extremely different, but I feel like from an end-user perspective it's superficially pretty similar.


Oh, I see, yes. The F# style is more powerful than the Go style, and can express the Go style if you choose. That’s a pretty good argument that it is superior, but Go might not be able to grow to adopt it


Yeah exactly. When there was only one or two risky functions in F#, I typically would just handle it manually with the match function like I described. If there was three or more (which isn't terribly hard if you're dealing with a bunch of streams from Kafka that you're manually merging), I would use the more monadic style. It made the code a lot smaller and more readable, and I really don't feel like it sacrificed anything in terms of readability. Like, at all!

There's an overhead associated with boxing and unboxing, so you might be paying a small performance penalty, but since most of what I was doing involved talking to a network a lot, that overhead was immediately dwarfed by IO latency anyway. I don't have any numbers on this, but I suspect that a very large percentage of "risky" functions involve IO of some kind anyway, so the overhead associated with boxing and unboxing is kind of negligible.

The lack of algebraic data types and nice mapping constructs is the main reason I almost never end up using Go for any personal projects. I still kind of like the language, I think CSP is a pretty decent concurrency abstraction, but it always feels like it's fighting me.


one of the alternatives to Go's error handling that OP writes about is the Rust Result type, which is the same as Either. OP seems to think this is the best of the alternatives to the way Go handles things, but that switching to a sum-type approach would be too difficult, given that it isn't compatible in any way with normal Go error checking and a lot of software has already been written in the old style.


This sounds really nice. Can you think of languages that implement it this way?


Rust's error handling is in my opinion perfect. An operation that can fail will return a Result<T, E> type, it will either be Ok(T), or Err(E). You can pattern match a Result, and decide what to do with it.

Or, more likely, you just want to pass that error back up to your calling function to decide what to do with it, so you just put a ? on the end of your function call like this:

    let result = potentially_failing_function()?;


I'm mostly familiar with it in F#, Scala, and Haskell, but I suspect any language with some functional roots has an equivalent.

The F# For Fun and Profit blog has a decent post explaining it. That blog doesn't appear to be maintained anymore, but it is archived: https://web.archive.org/web/20180329001511/https://fsharpfor...

You can find examples in Scala with `for` comprehensions as well.


java:

    Optional<String> result = returnsAnOptional();
    if (result.isPresent()) {
        log.info("result is {}", result.get());
    }

    // and

    returnsAnOptional().map(r -> log.info("result is {}", r);


Optionals are a bit different though, since they don't provide a way to explicitly handle errors, just wrap the `null` value if it exists and force you to be more explicit about it.

I don't know that there's a clean way to do the Either monad in Java without algebraic data types. You could certainly make a wrapper class that maybe has an optional `Left` and `Right` private variable and then make a `map` that handles the unboxing of those, but I think that might be a bit messy.


my post was mostly facetious, but it is as you say: possible but probably not something anyone would want to use


Actually, I almost want to give it a try now. There's some messy try-catch Java code at work that might benefit.

Don't worry, I won't deploy it until my team has seen it ;)


Rust does.


The author writes about the Python exception: "Note that for any sufficiently complex program that invokes many dependencies, this stack trace will be so far down the chain that you may not even see where you’re making the call that causes it."

I never encountered this problem with Python, not even in a project with more than 1 million lines of code. Are there any real examples where a Python stack trace is truncated in such a way?

It almost sounds like his whole reasoning is based on the assumption that he won't be able to understand an error in Python when looking at a stack trace.


Many levels of try/except nesting could (in theory) lead to this, but my experience is the same as yours, I've never had an issue with a stack trace pushing the relevant data out of the buffer


    > Many levels of try/except nesting could 
At least to me, this is an anti-pattern in any language that supports try-catch type error handling.

Many levels of try-catch should almost always be eliminated except for a small handful of use cases.

Unless you 1) can recover, 2) can retry, 3) need to log some local context, 4) need to transform the lower level error (including transforming it for display purposes), my heuristic is that it should always be handled at the highest level possible in the stack and just let the exception pass through.


Agreed! I normally just raise exceptions all the way up until the method that called the function that errored, then handle the exception at the highest level. That way, I just get a stack trace if I don't have any handling. But I'm also an amateur, self taught developer who's probably doing most things wrong :p


The point of chaining exceptions is to add information to an error result. It prevents one from having to pass down information to produce an error message that makes sense to the user.


That really falls into "3) need to log some local context" which could be parameters, state, etc.

But generally, for the user, I posit that it should still be handled at the highest level of granularity and the message returned to the user on an error should be similarly high level.


Thanks for the reply.

I try to use exception chaining to create a message that is useful for the user to actually debug the problem (solution-directed error message).

The classic toy case is either getting back "permission denied" or "can not open foo" but not both. Chaining of error messages gives a straightforward mechanism for this.

Then, the high-level text, along with the caused-by messages, is displayed without a stack trace to hopefully give the user enough information to address an issue.

Chaining can be done with explicitly returned errors as well as exceptions. The hard part is actually thinking about "is this use to the end user"


same. in fact, with python3.11, stacktraces are absolutely amazing.

in his python example, it shows exactly where the error is (down to pointing the function), and the error is quite explicit.


Part of the issue is that when people evaluate a language, they do it in a safe setting: a toy problem, a coding challenge, whatever. In those cases, handling error scenarios is annoying and just slows you down.

But when the rubber meets the road and you are trying to write code that consistently works, Go’s verbose but simple error system is really an asset. I used to write a lot more Go and have been writing Python, and I miss Go’s style terribly. Errors as values, the build/package tools, gofmt, and the consistent quality of the standard library are what I miss most.


That's true, but it's also true than when people evaluate a problem, they tend to want to imagine the maximally scaled, most robust, "final" version thereof. And so they tend strongly to solve the wrong problem in practice, often in inappropriate languages.

Now, for the record, I happen to love Go and agree with the author about its error handling paradigms. That said, I like the python script at the top of the article a lot more still, because the problem being presented is a routine kind of thing that needs to be solved on a routine, ad-hoc basis a zillion times every day. And we don't want people writing the 80 line Go code to do that, we want them doing it in python.

Basically exception-style "I don't know what went wrong, just that it failed, let the user sort it out[1]" code is the correct solution to probably 90% of workaday problems we face. You fall through to precise error handling (and/or frameworks like in Rust that force the handling syntactically) only when you need to write code that survives in the absence of a traditional "operator". But most code doesn't run that way!

[1] In this case, the problems are likely bad filenames, bad URL, server down, etc... If your developers are running this script themselves, they'll figure that out on their own.


To me, the advantage isn’t so much that you have to handle every error. It’s that you can see, with certainty, which functions and methods are error-free. I even find myself encouraged to write functions that cannot return errors.

You don’t get anything similar in exception-based systems.

Of course, algebraic error types share this advantage. They are good too.


Well, you can simply write "result, _ = function()" to ignore the error for the time being, but that underscore will sit there nagging you to handle the error properly...


There's also tooling you can run that scans for such code and points it out to you. I've seen people integrate such tools in their CI/CD as well.


You can ignore errors in most languages, rust included.


I extracted out all the err-checks up to where they're handled.

  if err != nil {
    return nil, err
 
  if err := json.NewDecoder(response.Body).Decode(&data); err != nil {
    return nil, err

  if err != nil {
    return err

  if _, err = reader.Read(); err != nil && err.Error() != "EOF" {
    return err

  if err != nil {
    if err.Error() == io.EOF {
      break
    }
    return err

  if err != nil {
    fmt.Printf("Failed to fetch data from %s: %v\n", url, err)
    continue

  if err := processCSV(csvFile); err != nil {
    fmt.Printf("Error processing CSV: %v\n", err)
Everything just bubbles-up. You've reinvented Exceptions with more boilerplate. Nothing here is 'handled' beyond that.

Although I don't read Go, it looks as if main() returns success to its caller even in the failure case.

So the Python code "which doesn't handle exceptions" exits in failure properly with a good error message and stack trace. The Go code "which handles all aforementioned errors" returns success for a failed operation, printing a less clear error message than the Python.


To be fair, this is horribly written error handling. These errors should be wrapped to add the specific context that the caller might want in order to handle the error.


> To be fair, this is horribly written error handling.

I'll can only take it as it's presented, in good faith. The author chose what to publish and compare. If I've been trolled, so be it.

> These errors should be wrapped to add the specific context that the caller might want in order to handle the error.

That would be an Exception and its stack trace.


You're getting hung up on details of code written by ChatGPT, you do realize that, right?

It's a singular line change to return proper OS exit code on error in the main function. And what error message is clearer is not an objective fact but only a matter of preference.


> You're getting hung up on details of code written by ChatGPT, you do realize that, right?

I'm getting hung up on what the author chose to publish.

The author asked,

>> do you want to know when something bad can happen in your program, or don’t you?

and then published some Python code and some Go code. Of the Go code he wrote:

>> Wouldn’t you know it, we have all the aforementioned errors handled!"

It's not some subtle gotcha, it's the point of the article and he somehow managed to demonstrate the opposite.

It would be like switching from garbage collection to C to make sure you remember to free all your mallocs, then showing demo code that's only a singular line change away from returning all its memory.


I still maintain that this code is one line of fix away to satisfy your requirement of a proper OS exit code, which you didn't address in your comment.

And readability. It's quite subjective and I can work with both the Python add Golang errors.


Go's error handling is good, but rust's error handling is phenomenal. It just doesn't really show in an 18 line program... Rust and Go both force you to acknowledge the possibility of errors in your code, whereas python and ruby, etc, will happily let you ignore them until something blows up.

Being able to use `?` in a method that returns `Result<T, Err>` is so elegant and helps readability compared to multiple if err != nil statements in go. In a larger program, ? lets you pass along errors up along the chain until you get to a place where they actually need to be handled, and this lets you consistently deal with them as well. For example, in a slightly more complex example [0], I handle errors on line 162 from monitor_device() (line 176, 181), instead of cluttering up monitor_device() unless I need to. (This was just some prototyping code so please bear with me).

[0] https://github.com/andrewzah/fst_rs/blob/master/src/main.rs#...


> do you want to know when something bad can happen in your program, or don’t you?

No actually, I want to know when consequential errors that I can be responsible for happen.

I don't really care that reading a file could fail (and it can fail in such a variety of ways, there's like 3 dozen distinct errors that could be thrown!) because I can't do anything about that. But if there are errors that I could recover from, that I would care about.

Unfortunately while it's possible, very little code distinguishes between error kinds or returns custom errors in go, so your bet is usually parsing error messages, while in Python or rust you have the ability to handle errors that are relevant to you, and let irrelevant ones percolate away.

Go on the other hand encourages doing nothing except logging, because it's somewhere between difficult/unidiomatic and impossible (depending on the libraries your building on top of) to handle only certain kinds of errors.

As a concrete example, in that csv parsing code, how do you differentiate between a file not found, file too large, and lacking permissions to read it? The stack overflow consensus is that doing that is unidiomatic, and you should just print the error to the user, which means this is just boilerplate for an exception that will ultimately gracefully end processing and print an error/stacktrace to the user. A bunch more typing for precisely the same intent as you get for free in Python or Java.


I do like that Python has both typed exceptions and "untyped". So you can classify your exceptions and catch or throw them easily.


Having an AI generate Python code without error handling, then Go code with error handling (that's also ~6x more code, mostly because of all the verbose error handling) is an absolutely wild argument to use to defend Go's error handling.


Since the prompt doesn't mention error handling explicitly, it does show a difference in the general attitude towards error handling in the Python code vs. the Go code in the corpus


It kind of reads like satire, to be honest.


I don't necessarily disagree with the overall point, but I'm sad the article misses the big advantage to the sum-type solution. It is not just about "being able to collapse `res, err := func()` into `res := func()?`". With sum types you literally cannot access the Result's inner value without performing some kind of error handling. Even if you just `.unwrap()`.

from https://fasterthanli.me/articles/i-want-off-mr-golangs-wild-...

> The point is, this function signature makes it impossible for us to access an invalid/uninitialized/null Metadata. With a Go function, if you ignore the returned error, you still get the result - most probably a null pointer.


"You’ll mainly see people complain about having to litter their apps with if err != nil, how they feel it makes the code verbose, and how they miss just writing ten very consequential lines one after the other in more dynamic languages like Python or Javascript."

One of the practices you'll see better Python devs use is to wrap stuff in try-catch-except blocks, especially around consequential code.


Go's error handling is very much not perfect. There's no reason for it not to have a bunch of syntax sugar for extremely common patterns.

See all the syntax sugar C# has for handling nullable types. That could easily be done for error handling as well. Or see Odin which has the exact same error handling mechanism as Go but with proper constructs to make it painless.


My takeaway from the article is that junior developers are intimidated by stacktraces.


The python example stack trace says explicitly what the problem is and somehow it's a problem?


Don't you get it? You're supposed to manually create the same stack trace by wrapping your errors! Isn't this much more readable?

Error couldn't parse data: couldn't read csv : file not found

Manually appending what you're trying to do at each level that checks for errors seems... Error prone.


I know I'm doing something wrong, but trying to programmatically figure out errors in golang always seems to rely on knowledge of a magic string in the error and I find it frustrating


Leaving aside the technical merits of Golang's error handling, this article is a straw man. The Python example can be rewritten with error handling. And arguably in this example the Python program is fine.

There is no objectively "perfect" approach. I personally prefer getting a stack trace of the point at which the problem occurred. I find that more useful as opposed to getting back an error code which has been propagated and modified through layers of function calls and is often hard to correlate with the original error condition.


> I also ascribe that issue to the subject of nil in Go, which is a whole other topic, this article is about errors.

I’ll bite. Idiomatic Go code, as in the article, does:

    response, err := somethingorother()
In the error case, response needs to have a value, and nil serves this purpose. If response were a non-nillable type, this would not work.

Result-style types solve this problem.


Wait, the Go error handling on that code has the exact same functionality as the Python one. All it does is making the code about 5 times as long and way less extendable.

Way, way less extendable. You'll have to completely rewrite the Go code if you want to put it in a library.

Now I'm doubting the author discernment on building that opinion.


Either types are probably the best form of error handling.

I would agree that complaints about go's error handling are really silly because said complaints usually come from people that work in languages with exceptions where, if they choose to handle errors, need to handle them in precisely the same way as in go, just in an even more verbose form.

The only reason people coming from these languages don't like go is because go actually forces you to either deal with the error or explicitly ignore it rather than just letting your functions propagate failures, which in theory is nice but in practice leads to horrible user experience because the buck wound up not stopping anywhere up the call stack since every caller decided "someone else can handle it".


I write go.

As every programer does you sometimes need to write throw away code. The one off script, a quick tool to clean something up, a loop and a regex to make it cleaner data, aggregating data from two systems into one handy file.

Go has this handy way of throwing away an error return. If I have to write something that is trash I skip over the errors. I can ignore missing files or json formatting issues. Granted that code would not pass muster of a code review, it would be apparent what I'm doing.

You can cut corners in go, it just reminds you that your doing it.


Just as a point of fair comparison, here is their prompt for CSV parsing in Rust (brought to you by Gemini):

  use std::error::Error;
  use csv::Reader;
  use reqwest::Client;
  use serde_json::Value;
  
  #[tokio::main]
  async fn main() -> Result<(), Box<dyn Error>> {
      // Define the CSV file path
      let file_path = "data.csv";
  
      // Open the CSV reader
      let mut reader = Reader::from_path(file_path)?;

      // Create a new HTTP client
      let client = Client::new();

      // Iterate over each record in the CSV
      for record in reader.records() {
          let record = record?;

          // Get the value from the first column
          let url = record[0].to_string();

          // Perform GET request
          let response = client.get(&url).send().await?;

          // Check for successful response
          if response.status().is_success() {
              // Read response body
              let body = response.text().await?;

              // Parse JSON
              let json: Value = serde_json::from_str(&body)?;

              // Process the parsed JSON (e.g., print it)
              println!("JSON for URL {}: {}", url, json);
          } else {
              eprintln!("Error fetching URL: {}", url);
          }
      }

      Ok(())
  }

The thing that makes it perfect in Rust, that you can keep the above for a simple script, but if you want to solidify it you just think about how you want to handle the `?`-annotated spots (retries, error-repacking, panics, etc.)


I ran the same prompt through ChatGPT (3.5), and provided the go code to make it give use the equivalent factoring and the rust code is still more readable than go (

    use csv::ReaderBuilder;
    use reqwest::blocking::Client;
    use serde::Deserialize;
    use std::error::Error;

    #[derive(Debug, Deserialize)]
    struct Data {
        #[serde(rename = "key")]
        value: String,
    }

    fn fetch_data(url: &str) -> Result<Vec<Data>, Box<dyn Error>> {
        let response = Client::new().get(url).send()?;
        let data: Vec<Data> = response.json()?;
        Ok(data)
    }

    fn process_csv(csv_file: &str) -> Result<(), Box<dyn Error>> {
        let file = std::fs::File::open(csv_file)?;
        let mut rdr = ReaderBuilder::new().from_reader(file);

        // Skip the header row
        rdr.headers().unwrap();

        for result in rdr.records() {
            let record = result?;
            let url = &record[0];

            match fetch_data(url) {
                Ok(data) => {
                    println!("Data from {}: ", url);
                    for item in data {
                        println!("{}: {}", "key", item.value);
                    }
                    println!();
                }
                Err(e) => eprintln!("Failed to fetch data from {}: {}", url, e),
            }
        }
        Ok(())
    }

    fn main() {
        let csv_file = "your_csv_file.csv";
        if let Err(e) = process_csv(csv_file) {
            eprintln!("Error processing CSV: {}", e);
        }
    }
My prompt was:

Please write some rust code for me that goes through every row of a CSV file, performs a get request for the value in the first column, and parses the responses as a JSON map of strings to strings.

The code should be the rust equivalent of the following go code:... I also added at the end "Use reqwest for the http calls" as chatgpt spat out some code using ureq which was worse than the reqwest version it used without telling it to make the equivalent of the go code.

The main difference I see in the two is that as a go developer the language forces you to become intentionally code-blind to the error handling ceremony. `if err != nil` is effectively punctuation, but it's treated the same as your actual program by representing it as tokens.

Go treats the tuple(result, err) as a good-enough error handling approach. Rust says here's something equivalent but better.


> do you want to know when something bad can happen in your program, or don’t you?

Almost everything can fail, and we don't need our noses constantly rubbed in it. Knowing how it failed might be interesting, but you're on your own to check for error subtypes and it's rare to define any.

2/3 of the code is way too much boilerplate for delegating error handling when it can be automated more reliably.


Summary: thing people complain about is actually great except when it isn't.


Go's error handling is explicit which enforces the need to handle whereas other languages leave it to the developer. More like here's the mess take care of it vs everything looks clean but its really swept all under the rug and can bite you later. With growing dependencies and complexity increasing everyday, I'd rather think of it sooner than later.


It's explicit, but it doesn't actually enforce the need to handle it. We've had this crop up in our code base before in accidental sneaky ways, esp. when it comes to named returns and just the sheer quantity of if err != nil checks.

For example:

  err := doSomething()
  if err != nil {
    return nil, err
  }

  err = doSomethingElse()
  if err != nil {
    return nil, err
  }

  // Oops unhandled
  err = doSomethingElseEntirely()
  
  // A bunch of other stuff..

  return nil, nil
Would much prefer "Either" with pattern matching, or something else that actually forces you to do something.


Agreed.

Go forces you to think about failure just as much as success, and I find that fantastic for building robust software.

Errors happen, so you're forced to do something when they do. Errors aren't exceptional.

But more importantly to me, Go forces you to think about what a caller to your function might want to know about what went wrong. Errors in modern Go are for communication, not an explosion of "something bad happened, abort!"

A long time ago, errors in Go were very basic -- essentially just strings 99% of the time, and I think that's where some of the hate comes from. In the early days, I think it was deserved.

But nowadays, with the Go errors package (error chaining), errors are extremely powerful.

Good libraries will often return an error structure that provides more context than a stack trace would, e.g. os.LinkError: https://pkg.go.dev/os#LinkError

tl;dr if you're writing "if err != nil { return err }", you're holding it wrong.


> Go forces you to think about failure just as much as success

Except, it doesn’t.

Forcing you to at least write boilerplate code for failures might be a nudge to think about them for some people, but it absolutely is not “forcing” you to think about it, and you can absolutely defer it with boilerplate while concentrating on the success path and never actually return to it.


About that:

res, _ := some_func(…)


Sure, you can do that just as much as you can do "try! some_func()" in other languages.

But it's obvious that you're doing something naughty when you do.


You’re right. But that’s my point. You can “ignore” the error and Go doesn’t force you to do anything that about it. I’ve seen this in production code. Also I’ve seen people just checking the null-ness of the return value, ignoring the error. And that brings me to my point, which is, it’s all about good programming practices, whether it’s Python or Go.

edit: combating the overzealous auto-correct


I'm not the strongest Go programmer, so maybe my inexperience leads me to still disagree with the ergonomics of "if err != nil" over something like a formal `Result`/`Either` type à la Rust/Haskell/..., but this is a decent bog post.

I think using a more structured capital-R Result type, combined with match/case makes for better structured code, and I can't help but think that `err != nil` is a bit of a smell left over from Go's initial decision to avoid doing generics (all of which is alluded to in the writeup).

I do disagree with the author using a trivial example to demonstrate that `err != nil` isn't that bad, since most code you'll work with isn't trivial. the errnil pattern obviously isn't enough of a problem to deter many companies, which I've always been interested in, since to me it always seemed like a brittle and awkward error handling mechanism.

All that aside, `blog.verygoodsoftwarenotvirus.ru` is an excellent domain name.


In the article

> The only opposition I have to the Result type in Go is that we wouldn’t be able to make use of it in the standard library without either breaking backwards compatibility, writing Result variants of existing API calls (so NewRequest, NewRequestWithContext, and NewRequestWithContextAndResult), or issuing new /v2 variants of existing packages (like the recently-released math/rand/v2 package), which then means we’ll have some libraries and programs that use the old style with one return value, some with the new style, and many instances of confused programmers using the wrong one. It would be as close to a Go equivalent of the Python 2/3 transition debacle as I think we could manage.

I see Go's approach and Rust's approach as being mostly isomorphic as Go's `tuple(result, err)` maps pretty reasonably onto Rusts's `either(Err(err), Ok(result))`. If Go implemented a language convention that made these 100% equivalent, there would be no real problem except the battle between the old school and new school code authors as to which one is the right one to use in their code.

Caveat - I am not a Go programmer, so I have no idea how feasible this is as an addition to the language proper. If you wanted to write Rust like you do go, it would be as simple as the following (cursed):

    impl From<Result<T, E>> for (Option<T>, Option<E>) {
        fn from(result: Result<T, E>) -> (Option<T>, Option<E>) {
            match result {
                Ok(value) => (Some(value), None),
                Err(error) => (None, Some(error)),
            }
        }
    }
then

    let (val, err) = someFallibleCall().into();
    if val.isSome()
        // something good with val.unwrap()
    } else
        // something bad with err.unwrap()
    }
(noting of course this would be intentionally unidiomatic rust - real code would probably use match (val, err)) for this.


> The only opposition I have to the Result type in Go is that we wouldn’t be able to make use of it in the standard library without either breaking backwards compatibility, or (breaking backwards compatibility)

I'd say then that it's only as perfect as you can be while being compatible with existing Go code.


False dichotomy; cart before the horse?

My general approach is validate inputs before running "the thing".

Check if file exists

Parsing CSV file is moved to another class, do more validations on data

HTTP calls, if failed, shouldn't eject the entire algo.

FWIW, that Go code might be "safer" but it hurt my eyes.


I've come to the same conclusion. After teaching several other developers how to Golang, coming from the same general background as me, I found that as we grew in lines of code that Go's error value system was actually really really useful and helped develop more defensive programming styles. Being able to define your own errors and use those instead of instantiating a string every time or having non-standard error string formats.

It gets in your way at the beginning but once you get beyond a module or two, it's quite useful, novel, and under appreciated.


Much more in the camp of exhaustive pattern matching such as in Scala or Elixir. Makes my code much cleaner, and I avoid if statements and loops.


Well, maybe I’m not cool enough to appreciate it, and maybe I’m more accustomed to the style of other languages, but if I see more than a few ifs in a file, it feels like a code smell, and it feels like some refactoring might be good. But I’m hearing that this profusion of error checking “if“ statements is the go way.

To me, the ideal in a language is to assume that everything will go right, to enunciate the core intention of the program, function, etc., and to handle errors somewhere else, maybe below the main code or something like that. Mixing it all in right there with the code, even if we end up concluding that this turns out to be the best way to deal with things, seems visually confusing to me when reading the code.


I liked this article, and the discussion in these comments. I don’t understand why it was flagged. Is there a way to find out? Perhaps @dang summons an explanation?


I prefer switching on Erlang's (Elixir's, &c.) tuples.

  {:ok, result}
  {:error, reason}


This article basically just says two things:

1. Error handling is better than no error handling at all

2. Go is designed to be like Go not Rust

Yeah, your typical Go article...


What is with the site - `very good software not virus . ru`


I also found it suspicious, but it seems to be legit - the source of the blogpost can be found on github https://github.com/verygoodsoftwarenotvirus/blog/commit/6a43...


No mention of `errors.Is` or error wrapping? IMO that's where the pain starts.

I definitely agree that in many programs, when an error happens, you're done, you just blast up the stack and then the error message goes into a sentry report or gets logged etc.

But it's also still quite common to want to treat some error conditions as soft errors, or otherwise handle them differently. Now you're doing errors.Is and engaging with the poorly structured world of error wrapping trees.

Here's one I hit recently. One of my pet peeves is error messages like "unsupported thing". Ok, what thing was unsupported? Very often people just omit this stuff because it requires you to now cat strings, which is sometimes hard or cumbersome depending on the language. So, I changed my code to have the error be "unsupported thing: <thing>".

However, up the call stack, I want to handle ErrUnsupportedThing too. Because maybe there's some reasonable thing I can do to help the user make the thing supported. So I'm calling `errors.Is`. But wait, now I need an `ErrUnsupportedThing` variable to refer to. But each call to fmt.Error creates a new error that isn't `errors.Is`-equivalent to each other.

I ended up reconciling this by making `ErrUnsupportedThing` just be "unsupported thing" and then I wrap it by doing fmt.Errorf("%w: %v", ErrUnsupportedThing, thingType).

But now what if I want to say "ErrUnsupportedThing: <A sub-error that occurred>"? Well, hope you're using a recent version of go, because "%w: %w" only works in go 1.20 and later.

Also the fact that all errors are just "error" instead of being typed errors tends to make people a lot lazier about their error handling in general. In java, if I get a ThingNotSupportedException, I expect to be able to find a getUnsupportedThingType() so that I can explicitly pull it off the string without needing to parse. That's easy in Java, it's a subclass of exception. In go, now you're writing a "subclass" struct to implement error's interface, tacking on extra struct fields and stuff, and since nobody ever actually types their return values more explicitly than `error`, you're now doing casting/duck-typing to pull that information out. Etc.

I guess let me put it this way. Go errors are good for 80% of errors and fine for 95%. That's great. But the easy code is already easy! What irks me is when it breaks down when I get to the code that's actually a bit tricky to handle errors for. The type system, the error system, and everything is just like "wow good luck you're on your own". Whereas python/java/etc have my back here.


Agreed, 100%. Unfortunately a lot of solutions tend to just be "log the thing". Which now means you have to go log diving to answer a question that the language itself has thrown so many barriers at you to tell you.




Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: