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();
}
}