Oh no, don't even put them in the same sentence. I am no fan of Go, but its error handling approach is beautiful compared to PHP. Every time I call an internal function I have to go through the docs — does it return a NULL, an int, a boolean, or something else? Does 0 signal an error condition, or is it a valid value? Do I have to perform a strict check for NULL/false then? Or is it -1 (see link below)? The situation is generally better with third-party libraries though — they tend to just throw exceptions (if that matches your definition of "better").
You should. It's a perfectly valid exception handling mechanism. It's unfortunate that the name "eval" is overloaded for two separate behaviors:
* catch exceptions thrown as strings or objects
* compile and execute code from a string
Other than the name, they're different behaviors.For your example of openssl_verify see https://linux.die.net/man/3/x509_verify_cert
It can certainly be confusing but at least it is documented.
Also [1]:
>> "I don't know how to stop it, there was never any intent to write a programming language [...] I have absolutely no idea how to write a programming language, I just kept adding the next logical step on the way"
I assume he's better at it 16 years later.
While writing services I often need to centrally log an error and retry the process later on. Exception handling really reduces the mental load regarding error handling in this case.
On a semantic level, there are forseeable errors you need to check locally (for example wrong user input) or exceptional errors which can happen at any time (for example file access errors or out of memory scenarios). Exceptions are a perfect match for the second type.
I don't really understand since when or why exceptions are uncool. It hurts my eyes looking at all that error checking code in other languages.
Also, how about this wonderful syntax:
class OrangeSite
{
Someobject m_object;
public:
OrangeSite(Someobject o) : m_object(o) {
try {
// construct object
} catch (std::exception& e) {
// exceptions for anything in the body above
}
} catch (std::exception& e) {
// exception handler for anything that failed in the initializer lists
}
};
I mean, kill it with fire. That is the only way to catch exceptions that occur in initializer lists and yes you have multiple catches. C# et al haven't reinvented all the same mistakes, but still, there's still two ways to control flow: return values and exceptions.What is really needed is the ability to do the same sort of filtering on error types. You could actually do it in golang with a type assertion, but it would require implementing a type that implements the error interface and type assertions. It's ugly.
Regarding your errors/exceptions, I've heard this before and I agree with it. There are error conditions, from which the program can and is expected to recover, and exception conditions, from which the program cannot recover. Out of memory is a perfect exception condition.
However, this semantic distinction is entirely lost - a lot of error conditions are reported as exceptions. In my above example you have absolutely no choice in the matter; having no error type, languages like Java/C# etc heavily encourage, even if they don't mean to, return by exception.
The golang and rust mapping for these concepts is panic and... panic! respectively. You can catch and recover from panics if need be in golang; in Rust panic handling is bounded by thread and if a panic occurs on the main thread, the runtime triggers an exit. As far as I'm aware, the only way to recover from a panic is like this:
fn unrecoverable_error() -> thread::Result<()> {
return panic::catch_unwind(|| {
panicking_function()
});
}
with panic="unwind" set in cargo.toml. If the thread::Result seems bizarre. it's because panic::catch_unwind basically spins up a thread to isolate that panic.Thus panics in Rust really are for things that should absolutely crash the program there and then. Everything else is a result type and you can shove any type you like into the error field. Golang is a bit more forgiving in that panics can be caught, but the language still encourages the use of errors rather than panics.
At the end of the day, at some point there's going to need to be a translation between some of the internal errors and what the end user sees and this process is going to end up in some boilerplate, be it lots of classes implementing some exception superclass, type or interface, or a bunch of if statements to handle exceptions. I don't think this is avoidable and one line magic code makes me uneasy - I have some experience now managing developers who like magic and want to avoid, at all, costs, translating errors to have friendly user output.
Lastly, there are workaround to this sort of boilerplate. Here's a simplified snippet I use to write this stuff whenever I write go:
snippet errif "Call function then check error" w
${1:var}, err ${2::}= ${3:func}(${4:args})
if err != nil {
fmt.Println("$5 %v", err)
return err
}
$0
endsnippet
It's 2019, so, most editors support this kind of thing. I'm using vim, but VS Code supports it too if you feel the need for a whole browser rendering engine just to edit some text ;) the placeholders allow me to fill in the template. Every time I find myself writing boilerpolate code, I write one of these and automate the task as much as possible. vim-go has a bunch of them: https://github.com/fatih/vim-go/blob/master/gosnippets/UltiS... and I'm not sure how I've programmed all these years without this kind of thing. I have snippets for everything.Working with different kinds of people, beginners and seniors, I learned that everyone has their own way of thinking about errors. It's hard or even impossible to get a consistent code style. Code formatters like go-fmt surely help. But do you trust everone to handle each error case appropriately?
What I like about exceptions is, that our software handles each and every possible error now and in the future consistently in a fail-fast way with an accurate stacktrace for free. I can sleep well, because I can be sure our main loop won't crash. Everyone can write plugins without me moving a finger. It frees us to think about other things first and errors second.
But as a user I hate them and the reason a simple: Because the developer doesn't have to handle every case, many developers just don't know what types of errors/exceptions can happen at runtime and unless they catch all Exceptions at the top-level, their programs will crash every once in a while.
Yes, it is a bit unfair to judge a feature like exceptions by the resulting code quality, as you certainly can use them responsibly, but my personal impression is that exceptions mislead developers to writes code of lower quality (myself included). That is why I like the Go approach to (some extent).
And unless you catch the common ancestor for all exceptions, it’s difficult to know what you need to look out for. This is especially true in dynamic languages like Ruby.
To my mind, exceptions enter a language when the language lacks the semantics to expose them in the return value, where true/false/null/errno is too primitive for larger systems.
Option, Result, and Either are great alternatives that allow you to propagate problems to deal with them at a higher level, with explicit acknowledgment that an error might occur.
Exceptions can be ignored and caught in random places with little to no context, so long as they’re further up the stack. Or you just get stuff like:
try {
Application->runTheWholeThing();
} catch (e) {
print “something went wrong”;
}If you are catching error in random places, you are doing it wrong. As you pointed out, context is critical. My point is generally, the location of code that generated the error may not have sufficient context. If you have enough context locally (or through further inspection) on how to proceed, then you handle it at that level. If you don't have enough context, then you send it up the call chain.
In your code example above, if after that catch, the program exits, then I argue it is correct. Some exception that was unrecoverable happened somewhere in the application, thus it needs to halt. If the error is recoverable and the application should continue, the exception should be handled somewhere else in the application.
Isn't this is exactly what happens in Go code with the current implementation? At least all the Go code I've read seems to do this.
If you don't know what to do or can't do anything, you return the same error, preferably adding more details to the error string. If you do know what to do you handle it.
I much prefer that everyone has to make an explicit choice on what to do; I find following code with exceptions much more painful.
The community convention seems to be keep rewriting the `if err` dance, but there's nothing encouraging that in the language itself.
https://play.golang.org/p/3ujNLwOdXZf
But explicitly having to handle them versus implicit exception based handling is a tradeoff. Often it isn't clear which exceptions a function might throw unless you've read all the documentation or you're the one who wrote it. When I'm coding my own stuff I'd tend to prefer exceptions but in larger teams I'd tend to prefer explicit handling.
And then there are languages, e.g. Haskell and its Maybe, that manage to combine the best of both worlds.
Easy to deploy, easy to write, tons of docs on the official site, massive deployment bases, and also genuinely holding back progress in reliable software writing; both cover up the irreducible complexity in professional software development that other, fussier tools, force you to handle.
That said, exception handling syntax is ugly and cumbersome in most languages I've seen. Whether it's try/catch/finally with braces or begin/rescue/ensure/end or whatever. It's also rarely written in a way that tells the reader where exactly the exceptions come from, it's just a blanket for a large block of code.
Something that ties the handling directly to the statement that breaks, without the noise of including the 'exceptional' behavior alongside the main logic, might be an improvement:
result = do_something(x, y, z)
handle SomeArgumentError with my_nre_handler(x, y, z)
handle RecordNotFound with missing_record_handler(x)Sadly, the Go team apparently decided all of it was just too involved, so they picked the minimalist try().
[1] https://gist.github.com/networkimprov/961c9caa2631ad3b95413f...