You'd just concentrate on the "happy path", you'd close the file, there'd be nothing to forget or write blog posts about because the exception would be propagated, without needing to write any lines of code.
You'd just concentrate on the "happy path", you'd close the file, there'd be nothing to forget or write blog posts about because the exception would be propagated, without needing to write any lines of code.
Finalizers shouldn't fail because they might be executed while another exception is already in flight. Three languages have three different behaviors when that happens but in my opinion they all do the wrong thing:
In C++, if you throw from a destructor while another exception is in flight, the program will be terminated. In Java, throwing from a `finally` block will "forget" the original exception. In Go (according to this article, I'm not familiar with it), error from `defer` will be ignored. None of these are ideal.
Since function calls form a tree, exceptions must form a tree as well.
Doing this automatically is also one of the killer arguments for exceptions over error codes, IMO.
> Doing this automatically is also one of the killer arguments for exceptions over error codes, IMO.
Definitely doing this automatically is better than relying on the programmer to do this manually, but I wouldn't say this is the "killer" argument for exceptions over errors codes, because this doesn't add anything new to the argument of exceptions vs explicit error handling.
When I say "exceptions", I mean the source-level semantics, not how it's implemented behind the scenes.
What would be the right thing? Combining the original exception and the error from `close` into some kind of `MultipleError`?
I don't thing there's one true right thing™ though. That's why explicit handling is necessary: The compiler doesn't have enough context to handle it for you. The programmer needs to decide what's the right way to handle it.
>>> mylist = []
>>> try:
... first = mylist[0]
... finally:
... inverse_length = 1.0 / len(mylist) # imagine this was something more complex
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
IndexError: list index out of range
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
ZeroDivisionError: float division by zero class A:
def __del__(self):
1/0
There is no way to catch that exception. It will just print a warning with the exception but it can't be handled.Furthermore in Java since version 7 you can actually see both exceptions with the suppressed exceptions pattern.
try {
using var file = File.OpenWrite("test.txt");
file.Write("Hello, World!"u8);
}
catch (IOException e) {
Console.WriteLine(e.Message);
}
The exception handler is an enclosing scope for both file open and dispose (flush and close) operations. You can also hoist file variable to an outer scope to, for example, decide what to do if dispose throws for some reason. In practical terms, this is as much of an edge case as it gets, but can be expressed in a fairly straightforward way.This includes an exception when trying to open or write to the file. If I fail to write to a file, it will try to dispose the stream, which flushes it and closes the file handle. If disposing the file handle itself fails, which should never happen, the exception will occur in the finally block, which this exception handler catches too. If you need to disambiguate and handle each case differently, which is rarely needed, you can order try-catch-finally blocks differently with explicit dispose and different nesting. This, again, is not a practical scenario and most user code will just `using file = File.OpenWrite` it and let the exception bubble up.
Just simply not explicitly handling every single possible error is the correct choice in many scenarios - in which case it bubbles up to a general error handler, e.g. telling the user that something bad happened here and here.
Both have advantages and disadvantages, I’d say the more “modern” approach actually the opposite to what you state here, and is in my opinion the way to Go (pun int intended), though it’s also how Haskell does it. You’ll find the same philosophy in Rust, Zig, Swift and others which all build on the previous decades of throwing exceptions and how terribly that scales in terms of maintainability. Even in the “old world” like with Java you have Kotlin which does both.
(I might misunderstand your use of "though", though? It could be that you were just noting in passing how Haskell disagrees with all of these supposedly-"modern" languages, and instead leaned into the sane happy path semantics, thanks to monads.)
(edit: to be clear, though... I do not think exceptions solve this. I wrote a comment elsewhere on this thread about the semantics issue, but a few other people also wrote similar things while I was trying to type my overly-verbose reply ;P.)
It is literally C’s shitty `errno` with syntactic sugar.
Go's error returns are not sum types, but they are product types. The return signature (T, error) indicates that two values will be returned essentially as a tuple by the function: one of type T and one of type error. Error-returning functions are pure functions (though they typically perform other, impure operations).
There is no syntactic sugar (both for good and for ill). The type of errors is an ordinary interface, with a single method. Any type can implement that interface, including strings and structs and slices. Errors can have as many contextual details as needed, including nested/wrapped error messages, specific parameters of loop iterations, multiple errors rolled up from multiple operations, etc.
Go's error return is just an ordinary but common use of its multiple-value returns. You can write a function/method that returns three ints and no errors:
func (v Vector3) Splay() (x int, y int, z int)
You can even write a function that returns multiple errors: func DoThisAndThat() (thisError error, thatError error)
Try that with errno!Technically, yes, practically, it doesn’t tell you anything, as its most common usage is how it would be used as a sum type (either one or the other).
And yeah, I didn’t quite think of multiple return values, but that itself can be just syntactic sugar over an `out` parameter.
But these technical details aside, I am not convinced that it is not “as useless as errno-type error handling”, with the only caveat of it returning an interface that is slightly more informative.
Yeah, it is - a bad choice IMO.
I know the "if err != nil" pattern is spoken up as some sort of cultural idiosyncrasy of Go, similar to the whitespace formatting in python. But so far, I haven't seen any actual data (or even arguments) why it is superior to exceptions, or which inherent problems of exceptions it solves. (The classical example of "it makes control flow more obvious and makes it easier to ensure that mandatory finalizers are not skipped" was just disproven by this very article)
So if there is more substantial criticism against exceptions than just FUD, I'd like to know it.
OK, here's an argument.
- In order to write resilient software, programs must handle not only the "happy path" when things succeed, but the path where things might fail.
- Thus it is important for developers to 1) be aware of which operations may fail fail, and b) think about what the program should do in that case.
- Exceptions make it easier for the programmer to forget that something might fail, and to avoid thinking about what to do if it does fail.
- Go's error handling idiom makes it clear that an operation might fail, and prompts programmers to think about what to do in that case. (They may of course choose not to think about it, but at least they made a conscious choice at some level.)
Thus Go's error handling idiom nudges developers towards more resilient software than exception-based workflows.
Or to put it differently: Programming systems which may fail simply is ugly: there are an exponential number of ways a system may fail, and each one must be handled correctly. Exceptions hide this ugliness, but by doing so make it more likely that there will be cases not handled correctly. By exposing this ugliness, Go makes it more likely that most cases will be handled correctly.
And exceptions let you handle error conditions without making the actual business logic harder to read, with as little or much specificity as required.
> Thus it is important for developers to 1) be aware of which operations may fail fail, and b) think about what the program should do in that case
Checked exceptions/effect types exist, being explicit or implicit in function signatures is not a fundamental property of exceptions.
And what is clearer in terms of error handling — if err being every third line, with questionable handling logic, e.g. just printing or swallowing stuff (or gestures at the article), and definite human error from repetition —— or a well-defined block with proper scoping, without which the error case does the only reasonable thing — automatically bubbles up, making it possible to handle higher up. There is often no immediate action that can be done in certain exceptional situations, e.g. your ordinary function that writes a file can’t do anything about a full disc. The best it can do is to yell, so that the action that called it somewhere can do some evasive action, e.g. re-trying/notifying the user/etc.
> Exceptions make it easier for the programmer to forget that something might fail, and to avoid thinking about what to do if it does fail.
Disagree. If anything, something not being in a try-catch block says that it will be handled higher up (or checked exceptions making it part of the signature), and when it’s surrounded by it, I know what is the happy path, and unhappy path immediately, without it being crossed over (usually badly), as it would happen with if errs.
> Go's error handling idiom makes it clear that an operation might fail
What about the case when it both returns a value and an error?
> and prompts programmers to think about what to do in that case
Blindly if erring and printing out a random string is not error handling. That’s just noise, and a terrible trap for yourself, having to grep for useless error codes later on.
I don't think you get what I'm saying. Some functions will always succeed. Some functions fail in obvious ways. Some functions fail in non-obvious ways. How do you know, as you're scanning a long block of code, which operations may fail, and which will always succeed?
For instance, suppose you have code like the following:
// Decode they key JsonKeyGuids as type []Guid
guids := JsonGetKey[[]Guid](&ru[i].Json, JsonKeyGuids)
Without looking at the function signature: If the key in the structure doesn't exist, what happens -- does it throw an exception, or return an empty value? Is it possible for JsonGetKey to fail to parse?And while checked exceptions might help, it's not perfect: Suppose your code block calls functions a(), b(), and c(); all of them return ErrParseFail, but while it's pretty obvious that a() or c() might fail that way, it's not at all obvious that b() would.
Secondly, even for operations that are obvious may fail: maybe you, as a senior programmer who has programmed with exceptions for years, are paranoid enough that you're always thinking in the back of your mind "what happens if this fails?" But I very much doubt a junior programmer is going to have that habit. Part of the intent of Go was to have a language for Google which would allow junior programmers could come to a previously unfamiliar bit of code and be reasonably effective very quickly.
> And what is clearer in terms of error handling — if err being every third line, with questionable handling logic, e.g. just printing or swallowing stuff (or gestures at the article), and definite human error from repetition
You'd almost never just print the result of error messages unless it's at the top level, or it's the equivalent of a script. In most cases, you bubble it up, often wrapping it with a message of what you were trying to do; e.g.:
return nil, fmt.Errorf("trying to excluded guid list for %v: %w", ru[i].Id, err)
That way at the top level (or wherever you do log the message), you have a stack not just of the function names and line numbers, but what the program was actually trying to do, potentially with specific values involved.Not having the equivalent of C's "must_check" is certainly a missing guard-rail in golang
> And what is clearer in terms of error handling...
It comes down to a judgement call. I think Golang's way is better. Yes, it makes the code look cluttered with exit paths, but that's because the code is cluttered with exit paths.
I can see that with experience, an exception-based developer would learn to see the implicit exit paths in most cases. So let me assert to you, that with experience, a check-the-return-values based developer also learns to filter out the explicit error paths to see the "happy path" algorithm clearly. But on the whole, I think the latter is likely to lead to fewer bugs, particularly for less experienced developers, but even for more experienced developers.
At any rate, now you've heard arguments for Go's error handling idiom; and if you don't agree, at least you can understand where the Golang crowd are coming from.
It’s not like exception handling and throwing around things to have them caught later is inherently bad. It’s just a different philosophy, one that I don’t personally like anymore. It’s down the same alley as things like OOP, SOLID or DRY. Things which have good concepts that way too often leads to code bases which are incredibly annoying to work with. Maybe not for small systems with short life times, but for systems where you’re going to be the 100th person working on something that’s been running for 30 years it’s just nice to not have to play detective. I’d like to put a little disclaimer in here, because that isn’t inherently a consequence of exception handling or any of the other concepts but it’s just what happens when people work on code on a Thursday afternoon after a tough week. The simpler less abstract things are made, the easier it’ll be to unravel, and simple error handling is dealing with the errors exactly where they occur.
As others point out, it’s not without its disadvantages. It’s just that in my experience, those are better disadvantages than the disadvantages of implicit error handling.
Then consider that if any code you're calling is not exception-safe it makes the task of writing your code to be exception safe that much harder.
Then add on top of that the question of responsibility -- do you handle the exception close to where it gets thrown or farther up the call stack? Handling it too close to the issue may be missing some context and result in a lot of the same exception handling across many areas of the codebase. But handling it too far up misses context, too, and often leaves the program in an uncertain state where it's not clear whether the show must go on or if it's time to shut it down. It's not uncommon to see a mix of exceptions and returned error values to try and find a happy middle ground there.
Java tried to paper over some of these problems with fewer gotchas (more explicit separation between resource allocation and initialization) and compiler checks that exceptions are in the method signature and are always handled _somewhere_ up the call stack but this often results in handling of very vague exception types at a high level. Some developers, uncertain what the right way to handle an exception is, and not wanting to crash the program, will just silently drop the exception instead! Admittedly, you can do this in go-style and C-style error handling, too, but at least then it's not as far removed from the source of an error because it's annoying to have to keep passing an error response through so many function signatures.
I used to use exceptions a lot in C++ and Java. This changed when I started working at a place where a lot of C++ is used but without exceptions. It was ostensibly about runtime costs but when seniors were pressed on the issue it was clear there were a lot of these other reasons stated above, and ultimately about readability of the code (error handling being close to error source) and a philosophy of failing quickly when assertions fail (because keeping calm and carrying on leads to programs running that should have died before they could make more of a mess).
I know it's unpopular but, in light of some real problems with exceptions, I actually prefer the way Go makes you do it. It often encourages doing all the setup in one (or a few) function scopes, and it becomes pretty evident from a function's signature whether you should expect something to go wrong or not. Would I prefer it for a game-dev scripting language? no! But for a system language it is better IMHO
It's faster. Doing all error handling via exception is not viable if you want speed.
Exceptions work well for errors in the sense that these rarely happen. But using them for general "this is the bad outcome" of an operation that can happen in the hot-path is problematic. For example pythons "next()" function which raises an exception if the end of the iterator is reached, if C++ did this it just wouldn't be used.
So in my opinion exceptions are nice if you want ease of use, and explicit error handling (look at rust with their question mark operation which makes it pretty easy to do) is the way to go for performance.
One advantage of explicit error handling like rust does it is: it forces you to handle the error path like other business logic. Again, if you write a short script, this is annoying and doesn't get you much but if the application is very important then doing so is a good thing since it forces you to think about where which errors can happen and how you handle them. With exceptions its very easy to completely forget that a can even happen and thus they get ignored and suddenly the program crashes with a unreadable error message.
(As for "handling" errors, you should only have a few places in the entire codebase which do that... littering the entire codebase with opportunities to feel like you might could handle an error seems like a mistake as it just encourages more handling.)
defer func() {
if r := recover(); r != nil {
fmt.Println("Failed to write file", r)
}
}()
f := os.Create("file")
defer f.Close()
io.WriteString(f, "Hello, World!")
Cool. You've solved one problem layer, perhaps. But if you look closely you'll notice that code still has a bug!So clearly exception handlers aren't enough. There might be some good ideas in using exception handling, but if you strictly limit its use to errors you end up half-assing it. Why not go all the way and find a solution that works in all cases?
Do you mean the issue that if both WriteString() and Close() throw an exception, the one from WriteString() will be swallowed?
That used to be a problem, but has been solved in more modern implementations using suppressed exception tracking.
Think more carefully about what we're actually trying to solve. It is not just about errors.
If you focus on errors, you’re going to never get it. The whole idea here is that the problem is bigger than errors.
The Go designers have explained and talked about this decision many times. Some people don’t like it and maybe choose a different language. No one pushes FUD regarding exceptions. I get the impression that if you choose a Toyota you would think that your neighbor buying a Honda poses a criticism you have to address.
Discussed before many times on HN, such as:
https://news.ycombinator.com/item?id=4159641
Original article:
https://commandcenter.blogspot.com/2012/06/less-is-exponenti...
If under ‘every approach’ you explicitly exclude go’s terrible errno syntax sugar, and include exceptions and sum types, then yeah. There is zero advantage to go’s error handling compared to the proper sum typed solutions.
Go's designers have explained their decisions many times, I won't repeat their justifications here. Obviously they had to make choices in line with their overall vision for Go. They feel strongly that programmers should handle or at least acknowledge the possibility of every error, explicitly, right where it gets reported. Exceptions and sum types don't enforce that.
Of course, nothing stops you from building your own file handing package that overloads exception handlers to deal with errors. If it gains traction then it would prove the stdlib should consider a v2 API.
But that you already haven’t done so is telling…
Anyway, most don't. There is no difference from exception handlers in most other languages.
The syntax is a little different. Is that where you've become confused?
Are there actually ANY languages other than go that have coroutines, and try/catch/throw mechanisms, where you cannot throw across a coroutine boundary?
And why would exception handlers NOT work across coroutine boundaries, other than laziness on the part of implementers?
You wha...? The question was about goroutines, not coroutines.
Besides, you'll notice that exception handlers cross coroutine boundaries in Go just fine. Your random tangent isn't even correct. Where did you dream up this idea to the contrary? I know coroutines are still new to Go, only officially landing in the latest release (experimentally in 1.22), but you'd think that would also mean their behaviour is fresh in your memory.
I'll take your avoidance of the original question to mean that no other language does it either.
What are goroutines, other than peculiarly broken coroutines? (Notwithstanding your point that go has a non-broken implementation of coroutines at experimental release stage).
It is true that Javascript has a goroutine-like facility for executing coroutines on a seperate thread. But there are languages (c++, c# at least) where coroutines can execute on separate threads without suffering from the broken-ness of goroutines.
So it is telling. But I think what it actually tells is that people would have done it just use another language instead.
> If it gains traction then it would prove the stdlib should consider a v2 API.
Some library that behaves completely differently from the rest of the language and breaks all interop with the rest of the ecosystem will have a hard time gaining traction, no matter if the way the library does it is objectively better or not.
Probably for the same reason Rust does, and why it suffers much the same problem:
1. It is what was in vogue in the 2010s.
2. More importantly, the problem isn't limited to errors. What have you gained treating errors as some hyper special case when they aren't any different than any other value?
I think we agree that we can do better, but seeing errors as special doesn't get you there. We need something that understands the all-encompassing problem.
So, failing that understanding, if you're going to do something that sucks, you may as well choose the least-sucky option, surely? Exception handling brings a horrible developer experience. To the point that in languages where errors over exception handling semantics are the norm, you will find that most developers simply give up on error handling entirely because it is so painful.
> Some library that behaves completely differently from the rest of the language and breaks all interop with the rest of the ecosystem will have a hard time gaining traction
I'm not sure history agrees. Ruby was also of the return values over exception handling mind before Rails came along. Rails pushed exception handlers for errors and developers went for it. Provide an API people actually want to use, and they'll use it. What was common before is inconsequential.
I expect what you are really saying is that exception handling wouldn't actually improve this example case even in the best case, and in the worst case developers would end up giving up on error handling leaving such a package to be a net negative to a codebase.
Or monads, but that might be a step too far for the Go world considering the push-back against generics. When you have declarative error handling (a good thing imho) then monads really are the bees knees.
You do in Java. It's called Checked Exceptions. It's a binding API contract and communicates this well not only when writing the code, but also when reviewing it.
The answer here is that everything throws.
Any code can have a Null/Nil dereference error, any code can use an array and generate an out-of-bounds exception, etc.