If you want to handle some error in a way other than crashing, handle it. Web frameworks usually catch exceptions that arise while a request is being handled, finish the request, and keep handling new requests. You handle things more specifically only as needed.
I like that behavior so much more than having a program keep running in an unknown state unless you handle errors line by line (and value it more than keeping code execution where I can see it). I don't think my programming language knows what errors are very important, either — that depends way too much on context.
In Smalltalk, the default behavior for an error is pretty much business as usual, because exception handling is mostly just Smalltalk code running normally. In some Smalltalks, you can override the "top level handler" and ensure that an app never crashes. (An infinitely growing collection will exhaust heap and crash the program that way.)
I think assigning the error to _ is only necassary when you have multiple return values(one of which is the error) and you are interested in at least one of them.
e.g.
returnsTwoValues()
is legal x := returnsTwoValues()
is notmaps are also interesting because
value := someMap[key]
is valid and value, keyIsPresent := someMap[key]
is also valid.I'm in the first mode when I'm writing high-quality code to solve stable, well-understood problems.
I'm in the latter when I'm doing almost anything else. E.g., prototyping, exploring, pushing out a MVP for user testing, adding a quick-and-dirty version of a feature to get real-world feedback.
In the latter mode, I'm very tolerant of errors just as long as I can diagnose the problem quickly. The way I start is to catch exceptions at a very high level, tell the user something nice, and have myself paged. If something blows up, I fix it quickly. There's no sense in making code robust if I don't know if it will exist next week.
Basically, the two approaches are the same only if you have infinite time and money. Google does, so I guess Go makes sense for them. But if this fellow's take is correct, Go's much less interesting to me as a general-purpose language.
Though it works, one potential issue I found with it is that it does not give enough context as to where in the program the exception occurred. E.g. if I have more than one open() call (all for read mode) in the code, it will give the same error message for an exception occurring on any of those open() calls (as long as the exceptions all happen for the same underlying reason, such as "file not found". E.g.:
Exception occurred: IOError(2, 'No such file or directory')
Had you come across this and found any way to handle it? I though of passing some unique code for each case, but there seems to be no way to do it, because we are not calling the except clause ourselves - Python does it.
Edit: just thought of a (crude) way to handle that issue: declare a global variable, say, "location", and set it to a different numeric or string value at each place in the code where an exception may be thrown, or at least at one place, say the top of the function, in each function or method. And then in the single except clause, print the value:
print "location=", location
This will at least help narrow down the area of code in which the exception was thrown.
I use the high-level error handler to let me know when a failure has some impact besides dumping a stack trace on my screen. E.g., failed web request, batch job barfing, etc. And to present the user with some reasonable "we're on it" thing.
I believe I'm correct as no one has yet refuted the central problem discussed. The replies have mainly been justifying it.
Looking at c libraries and their example code, almost every time, the example lacks error handling. Its almost like people avoid doing error checking in C code because of the cumbersome amount needed to be done in order to check each and every time. If an example code would include error checking, the number of lines can easy increase 3-4 times the original size.
Go standardises this in a super clean way. Every method that can error will return an error. If you try to write a method that calls methods that can error and you don't return an error then you will be extremely aware of that. After a few days of Go it will be physically painful to write code that eats errors.
So what Go did was to take the return parameter style of error reporting, mix in multiple return values to make that not suck and then optimize towards the local maxima of programming comfort within this paradigm.
The result - as anyone who has been writing go for a few months can tell you - is something that works incredibly well. I'd say it works better then exceptions style all but that may be my personal preference.
That's a bold statement. Nice to meet ya! I'm no PC-Luser! ;-)
The PC-lusering problem is mentioned in "worse is better", so I would hope a fair number of folks know about it. Every good code base I've seen uses EINTR wrappers, but it is true that many folks I've met don't know about it.
Hey now.. it's not just C programmers, though. We have to use the same EINTR-retry loops in python too, sometimes:
https://github.com/git-cola/git-cola/blob/master/cola/decora...
I like C, Python, and Go. There's no reason to take sides, IMO. These are all highly effective tools.
It is very different. With a try/catch block, you can see the normal logic flow of your code in one place, separated from the error handling logic. If you wrap each and every line that can have an exception with it's own try/catch block, then yes, that is pretty much the same as handling explicit error codes. Which is why I consider that poor style in languages that have exceptions.
Separating error handling code apart from the logic flow of normal operation is simply not possible with error codes as function return values.
(If anything, that should probably be "errors", quotes included, because what people usually mean by the term is "easily-forseeable occurrence that I couldn't be bothered to write the code for".)
Exceptions are ideal for things that require aborting and unwinding to a point - usually a loop, like a server request handler or UI event dispatcher - high up on the stack. If the general behaviour is not to abort and unwind, it's not a good fit for an exception.
It's also why exception handlers should be very rare. There shouldn't be any more than a half-dozen in most programs. Error codes that need checking and aborting by hand at every function call boundary are a poor fit for replacing exceptions, because they optimize for the case where the error can be handled, rather than the usual case for exceptions, where the error cannot be handled.
From what I understand, the Go language team wanted to write robust servers, thus they wanted to "optimize for the case where the error can be handled."
But were programmers to consult the history of their compiler's output (representative summary: "ERROR ... ERROR ... FAILED ... WARNING ... ERROR"), it would be obvious that in fact errors are highly likely, even if you know what you're doing. So what makes the error case different from all the other cases?
You're quite right that exceptions promote giving up control to a higher level. And that's the right thing to do for exceptional situations. The ultimate higher level is the programmer who's going to have to fix the bug, or the admin who needs to fix the configuration problem - because they are the two main classes of errors for which exceptions are great. (The third class is a logical abort, and depending on the language and how good its idioms support exceptions, this may or may not be good practice.)
You get rich information about the error, including a call stack, the ongoing operation gets cancelled as soon as it occurs rather than muddling onwards and causing more damage. If you can foresee the error such that you can write an error handler, using the method that throws exceptions is probably not the right one - unless it's unavoidable owing to race conditions, or impoverished libraries.
It's very similar to exceptions, except you have to consciously choose to do it. That's a virtue to me.
Irrelevant with go, thanks to the `defer` statement.
I think this is a matter of what you prefer the program to do by default, when something clearly wrong has happened, but no error-handling code has been written: you may prefer such code to halt, or you may prefer it to keep on trucking.
If you use exceptions, you have to think about it IF you want the program to keep on trucking. Otherwise you can just let exceptional circumstances take care of themselves - if the program stops immediately, it cannot then behave incorrectly.
As a default failure mode, I do prefer the program to halt and decline to further cock up the situation (forcing me and my tests to deal with the condition) than for the program to silently swallow the error and continue as a default (forcing me to thoroughly check error codes everywhere up front out of pure fear that I will miss a condition).
This is just a convention and either one works.
If Go can become a better version of C then I might switch out C for Go. This isn't by any means going to cause me to give up Python because I use C for very specific reasons (when it is totally OK for reasons of speed or control to write a decent amount more code, and have it read more cryptically, and have to take care of many more low-level details).
Put another way, verbose code can be simpler if it is direct and explicit — what happens is what's on the page — just as succinct code can be more complex if it is magical or implicit — what actually happens depends on who is calling it, say.
I do hate to read code which uses exceptions as a normal part of program flow, it's very GOTO-like and remote and implicit.
But I don't write code that way and nothing is forcing me to write code that way. When exceptions are reserved for really exceptional conditions where a requested performance CANNOT continue, they are used much less frequently, and more locally and explicitly.
In short I think the problem is a matter of philosophy more than language facilities, and the big differences are within-language rather than between-language.
The reason I like to have exceptions used as a convention is that I think a better default for programs which have not yet covered some corner case is for them to decline to run, rather than to run in dishonor.
if ( callSql( s1 )
&& callSql( s2 )
&& callSql( s3 )
&& callSql( s4 )
&& callSql( s5 )
&& callSql( s6 )
&& callSql( s7 )
&& callSql( s8 )
&& callSql( s9 )
&& callSql( s10 ) ) {
endTransaction();
return true;
}
logAndAbortTransaction();
return false;
No need for exceptions at all, and it's very clean and clear what's going on.I wrote the same code to my young exceptions-indoctrinated colleague who asked the same thing like you, and he couldn't believe at the start that that's all -- that much he wasn't used to a plain control flow. It's even obvious how it can be generalized for any sequence of commands:
for ( int i = 0; i < n; i++ ) {
if ( !callSql( s[ i ] ) {
logAndAbortTransaction();
return false;
}
}
endTransaction();
return true;There's no advantage in exceptions, except when you use them for "hardware-like" generated exceptions (which is where they belong).
void migrate() {
try {
callSql(s1);
callSql(s2);
...
endTransaction();
}
catch(SQLException ex) {
logAndAbortTransaction(ex);
}
}
Now I have an exception to log, which means I have a stack trace which is going to tell me which statement failed rather having to play the guessing game. I may even get an explanatory message for free if the exception came with a message.For me, having exceptions over return codes has a number of advantages:
* Within a single object, you get much more information (error message and stack trace). You need to return two different values if you need both a message and a return code
* You don't want to handle the exception at the call site? no problem, just propagate it up the stack. On the other hand, if you want to try the same thing with return codes, you need a way to differentiate the return codes due an error in your migrate() function and the error codes resulting from a given callSql statement.
* Which brings us to... another benefit: enforced compile-time checking of the exception. You can't write code that is going to check for a DogException when the callee throws a CatException. But it's very easy to check for -1 when the actual code is -2. Of course you can alleviate this somewhat using static constants, but it's still a safety issue.
Obviously, exceptions are not all roses either, whenever you need a new one, you need to create a class for it. But the workflow interruption is not greater by an order of magnitude than adding a static constant somewhere to represent your new error code.
Edited for better formatting.
I assume you are right about C++ exceptions (the little C++ I do doesn't use them). Modern managed languages incorporate the stack trace in the exception (including Java).
I'm not sure how you propose to have both return code and error message in C++, return a struct with an error and a message? This also does not answer my points 2 and 3 about handling errors occurring at different layers and compile-time checking.
Errors can be handled at different layers (using normal control flow the same way as I already demonstrated -- simply writing ifs) and the error declaration can be checked at compile time in C++ exactly as C++ has (who would have thought that!) classes. Java also uses class infrastructure for that, exceptions are only "out of normal flow path" mechanism.
import com.foo.SomeOtherException;
public class MyException extends SomeOtherException {
public MyException(String message) {
super(message);
}
}
That's about as complicated as it gets (considering the example extends a custom exception instead of inheriting from java.lang.Exception). I'll note that your example doesn't declare any return code anywhere. Now, I'm curious to know how you would do the equivalent of this with error codes: public class MigrationException extends Exception {
public MigrationException(String msg) { super(msg); }
}
public class SqlException extends Exception (
public SqlException(String msg) { super (msg); }
}
public class Migration {
public void migrate(Statement stat, int migrationNumber)
throws MigrationException, SqlException{
if (migrationNumber <= 5) throw new MigrationException("Wrong migration number " + migrationNumber, should be > 5");
// throws SqlException
stat.executeNonQuery("INSERT foo INTO bar");
}
}
public class MigrationRunner {
public void run(Statement stat, Migration migration, int migrationNumber) {
try {
migration.migrate(stat, migrationNumber);
}
catch(MigrationException ex) {
migrationLogger.error("Bad migration", ex);
}
catch(SqlException ex) {
migrationLogger.error("DB error", ex);
sqlLogger.error("Error during migration", ex);
}
}
}
I'm on purpose not reimplementing an SQL driver here :)I'm not quite sure why you are arguing about "error declarations which can be checked at compile time in C++". Sure, C++ has exceptions, but I thought you were trying to make a point about return codes, not C++ exceptions vs language X exception. My point, on the other hand, is that it's very easy to do this:
#define SQL_ERROR 1
// whoops, same error code due to copy-paste
#define MIGRATION_ERROR 1
void handleError(int errorCode) {
// Too bad, this was actually an SQL error
if (errorCode == MIGRATION_ERROR) {
...
}
else {
// We'll never enter this branch
...
}
}
No compile-time check for that. Or even: #define MIGRATION_ERROR 1
#define SQL_ERROR 2
void handleError(int errorCode) {
// I actually meant 2 here
if (errorCode == 1) {
logSqlError();
}
} func A() error {
err := compute.Something()
if err != nil {
return err
}
return nil
}
Throwing errors up the return chain using error values is really very little different than throwing up the return chain using exceptions, except that now there is only one way to return, and it is always clearly marked. You return where you say "return". And you return what you said you would.I'm not sure what you think idiom means (Hello, my name is Inigo Montoya), but you never need one to do anything in code. In human language they capture some context and their meaning must be understood by rote or in terms of that context, but in code they are entirely explicit and work just as well before and after becoming idiomatic—which is solely a form of classification that people use and computers don't.
Normal exceptions, the kind that unroll the stack (and not deep magic like call/cc) are only "out of band" as an optimization. There is no behavioral difference between "save the last place you decided to handle errors, and jump there directly unrolling the stack at once" versus "unroll the stack by a series of buck-passing in band error returns, until you get to the last place you decided to handle errors". A compiler could implement exceptions by silently transforming them into Go form. And a Go compiler could silently transform buck-passing into traditional exceptions.
Who cares about compiler transformations? The issue is the semantic difference between return codes and exceptions. Nobody is asking for exceptions because they can't or don't know how to get to the correct scope to handle their errors, they're asking for them because they don't want to write all the boilerplate necessary to do so.
And yes, you can transform exceptions to chained returns. That would be a lot of work for terrible performance. You can't do the other transformation though. It would require being able to statically analyze the code path to determine where the errors are eventually handled, which is impossible in the general case.
func A() error {
return compute.Something()
} if (!herp_derp()){
...
}
or if (herp_derp() > 0){
...
}
and don't actually check any of the error cases that you should be checking, like return = -1, -2, -3 etc.
If you use exception handling, then suddenly catching errors becomes much easier, and writing code is faster, but it's not like your program is any more robust. If you catch an error up at the top, your program or function is probably just going to quit. Of course, you can add more code to deal individually with each of the exceptions in more robust ways, even to the point of wrapping each statement in its own try/catch block, but then it's no different in practice from returning error codes.
In the end, I just don't see much of a practical difference. In both cases, if you want to be a lazy or rapid programmer, errors will lead to your software just not working. And if you want to be conscientious about your errors, then you can do that in both cases.
Exception handling lets you handle errors "at a distance". It's convenient, but harder to keep track of. Returning errors keeps your code execution where you can see it, and you always know where it is. It's simpler, but more verbose.