Hacker Timesnew | past | comments | ask | show | jobs | submitlogin

I basically agree with you. I think return codes are simpler, and exceptions appear simpler because we're used to them. In reality they are a magical, out-of-band mechanism compared to plain old return. I contend that local and explicit is more strongly correlated with simplicity than remote and implicit.

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 used return codes before I ever used exceptions, and even chafed at exceptions. Exceptions CAN be simpler. It's just an expressive tool, it matters more what you do with it.

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.


Say you need to migrate a database to a new schema. You need 10 SQL statements to do this. Each statement may fail, in which case you want to write stuff in a log and abort the transaction. In this case, wouldn't you agree that writing your statements in a try/catch/finally block would result in clean, easy to understand code, as opposed to adding explicit error handling after each statement?


I wouldn't agree, as when I'd code it in C I'd have a function callSql which returns false and write the code you describe as:

    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;


While this code is nice and concise, the problem is that you don't automatically know which call failed or what the error was.


The loop variant does know which call failed, and could easily be modified to know what the error was (if the underlying callSql had more than just a boolean failed/success return value).


If you can, write a more concise code with exceptions which will automatically show you which call failed and what the error was. If we include all the declarations, I claim that your solution wouldn't be shorter or more readable than my C variant I'll write, which would only declare error code values, or even return strings as error reports.

There's no advantage in exceptions, except when you use them for "hardware-like" generated exceptions (which is where they belong).


That's very easy:

  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.


Doing exactly the opposite from what I asked, you omitted all the declarations of all the classes you use. Note that if I use C++ I can also use the classes with the same semantic and that also contain error messages. The stack trace you mention is a debugger feature, not something your classes do by the language definition (I speak about C++, I don't know what's in Java). If you'd include all the code needed for your lines, you'll see that I have more freedom and not less expression possibilities. If you claim that you already have everything in libraries, OK, but C++ libraries without exceptions can have equivalent functionality. Exceptions are absolutely not needed to write the clean code that handles all the error conditions.


The example code is not using a new exception, it's handling the callee's, the same way that your code does not use a new return code but relies on whatever the callee returns and does not include the code for "callSql".

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.


I'm sorry but I firmly believe you completely missed the point. I wrote "If you can, write a more concise code with exceptions which will automatically show you which call failed and what the error was. If we include all the declarations, I claim that your solution wouldn't be shorter or more readable than my C variant I'll write" and you managed to omit everything I proposed as needed in order to demonstrate what is actually involved in exceptions -- not the use, but all the classes etc needed. In short, if you actually use library exceptions, there are thousands of lines of declarations. You can claim "but they are already there, I don't have to write them" still it's a library thing, not the "language as such" thing.

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.


Thousands of lines of declarations?

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




Consider applying for YC's Summer 2026 batch! Applications are open till May 4

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

Search: