views:

851

answers:

6

It seems to me Google's alternatives to exceptions are

  • GO: multi-value return "return val, err;"
  • GO, C++: nil checks (early return)
  • GO, C++: "handle the damn error" (my term)
  • C++: assert(expression)

Is multi-value return useful enough to act as an alternative? Why are "asserts" considered alternatives? Does Google think it O.K. if a program halts if an error occurs that is not handled correctly?

Effective GO: Multiple return values

One of Go's unusual features is that functions and methods can return multiple values. This can be used to improve on a couple of clumsy idioms in C programs: in-band error returns (such as -1 for EOF) and modifying an argument.

In C, a write error is signaled by a negative count with the error code secreted away in a volatile location. In Go, Write can return a count and an error: “Yes, you wrote some bytes but not all of them because you filled the device”. The signature of *File.Write in package os is:

func (file *File) Write(b []byte) (n int, err Error)

and as the documentation says, it returns the number of bytes written and a non-nil Error when n != len(b). This is a common style; see the section on error handling for more examples.

Effective GO: Named result parameters

The return or result "parameters" of a Go function can be given names and used as regular variables, just like the incoming parameters. When named, they are initialized to the zero values for their types when the function begins; if the function executes a return statement with no arguments, the current values of the result parameters are used as the returned values.

The names are not mandatory but they can make code shorter and clearer: they're documentation. If we name the results of nextInt it becomes obvious which returned int is which.

func nextInt(b []byte, pos int) (value, nextPos int) {

Because named results are initialized and tied to an unadorned return, they can simplify as well as clarify. Here's a version of io.ReadFull that uses them well:

func ReadFull(r Reader, buf []byte) (n int, err os.Error) {
  for len(buf) > 0 && err == nil {
 var nr int;
 nr, err = r.Read(buf);
 n += nr;
 buf = buf[nr:len(buf)];
  }
  return;
}

Why does Go not have exceptions?

Exceptions are a similar story. A number of designs for exceptions have been proposed but each adds significant complexity to the language and run-time. By their very nature, exceptions span functions and perhaps even goroutines; they have wide-ranging implications. There is also concern about the effect they would have on the libraries. They are, by definition, exceptional yet experience with other languages that support them show they have profound effect on library and interface specification. It would be nice to find a design that allows them to be truly exceptional without encouraging common errors to turn into special control flow that requires every programmer to compensate.

Like generics, exceptions remain an open issue.

Google C++ Style Guide: Exceptions

Decision:

On their face, the benefits of using exceptions outweigh the costs, especially in new projects. However, for existing code, the introduction of exceptions has implications on all dependent code. If exceptions can be propagated beyond a new project, it also becomes problematic to integrate the new project into existing exception-free code. Because most existing C++ code at Google is not prepared to deal with exceptions, it is comparatively difficult to adopt new code that generates exceptions.

Given that Google's existing code is not exception-tolerant, the costs of using exceptions are somewhat greater than the costs in in a new project. The conversion process would be slow and error-prone. We don't believe that the available alternatives to exceptions, such as error codes and assertions, introduce a significant burden.

Our advice against using exceptions is not predicated on philosophical or moral grounds, but practical ones. Because we'd like to use our open-source projects at Google and it's difficult to do so if those projects use exceptions, we need to advise against exceptions in Google open-source projects as well. Things would probably be different if we had to do it all over again from scratch.

+10  A: 

Multiple returns are not unique to Go, and they're not a substitute for exceptions. In C (or C++) terms, they are a concise and user-friendly substitute for returning a struct (object) containing multiple values.

They do provide a convenient means of indicating errors, if that's all you mean.

Why are "asserts" considered alternatives?

Asserts are initially for debugging. They halt the program in situations where it is in an "impossible" state, one that the design says should not happen, but which has anyway. Returning an error is unlikely to help much. The code base obviously doesn't work yet, so how on earth can it successfully recover? Why would you even want it to, when there's a bug that needs attention?

Using asserts in production code is a bit of a different matter - obviously there are performance and code size concerns, so the usual approach is to remove them once your code analysis and tests have convinced you that the "impossible" situations really are impossible. But, if you're running code at this level of paranoia, that it's auditing itself, then you're probably also paranoid that if you let it carry on running in an "impossible" state, then it might do something dangerously broken: corrupting valuable data, overrunning a stack allocation and perhaps creating security vulnerabilities. So again, you just want to shut down as soon as possible.

The stuff you use asserts for really isn't the same as the stuff you use exceptions for: when programming languages like C++ and Java provide exceptions for "impossible" situations (logic_error, ArrayOutOfBoundsException), they unintentionally encourage some programmers to think that their programs should attempt to recover from situations where really they're out of control. Sometimes that is appropriate, but the Java advice not to catch RuntimeExceptions is there for a good reason. Very occasionally it's a good idea to catch one, which is why they exist. Almost always it's not a good idea to catch them, meaning that they amount to halting the program (or at least the thread) anyway.

Steve Jessop
Yes, that's what I mean, a way to indicate an error condition (up the call stack) that doesn't disturb the existing logic too much.
reechard
Depends how you're using the return value. Much of the time you don't need the full range of the return type. If you're returning the number of employees of your company, then I don't think (number, iscorrect) gives any difference at all in the logic from returning number, or -1 for error. It does make it harder for the caller to ignore the error case, but their logic for handling the error is the same: if (something) then (deal with it).
Steve Jessop
@Steve: not quite, with exceptions you can have a block of logic code bundled together and process the error conditions at a later time (`catch`). This makes a difference as the logic is bundled together and thus easier to follow. I remember some homeworks back at school in C where for each line of actual code there was an `if` and a couple of lines of error processing that always resulted in basically the same: a message to the user and returning a in-band error token to the caller, when exceptions would have propagated nicely and could easily be processed where it makes sense.
David Rodríguez - dribeas
I wasn't clear - I don't think the logic is different between (a) multiple returns, (b) single returns with error sentinal values. I do think the logic is different between (c) either of those, and (d) exceptions.
Steve Jessop
Did I say "call stack" ? I meant "indicate an error as far up the call graph as required" -- which could be all the way to main(), if you refuse to "handle" errors at all.
reechard
Reading between the "Google lines" I think they leave C++ assertions live in the production code; assertion failures are errors which are not handled, so they log something somewhere and exit the thread or program.
reechard
+2  A: 

Yes, error return values are nice but do not capture the true meaning of Exception handling...that is the ability and management of exceptional cases in which one does not normally intend.

The Java (i.e.) design considers Exceptions IMO to be valid workflow scenarios and they have a point about the complexity of interfaces and libraries having to declare and version these thrown exception, but alas Exceptions serve an important role in the stack domino.

Think of the alternative case where exceptional return codes are conditionally handled in several dozen method calls deep. What would stack traces look like in terms of where the offending line number is?

Xepoch
+2  A: 

You should read a couple of articles on exceptions to realize that return values are not exceptions. Not in the C 'in-band' way or in any other way.

Without going into a deep argument, exceptions are meant to be thrown where the error condition is found and captured where the error condition can be meaningfully handled. Return values are only processed in the very first function up the hierarchy stack, that could or could not how to process the problem. A simple example would be a configuration file that can retrieve values as strings, and also supports processing into typed return statements:

class config {
   // throws key_not_found
   string get( string const & key );
   template <typename T> T get_as( string const & key ) {
      return boost::lexical_cast<T>( get(key) );
   }
};

Now the problem is how do you handle if the key was not found. If you use return codes (say in the go-way) the problem is that get_as must handle the error code from get and act accordingly. As it does not really know what to do, the only sensible thing is manually propagating the error upstream:

class config2 {
   pair<string,bool> get( string const & key );
   template <typename T> pair<T,bool> get_as( string const & key ) {
      pair<string,bool> res = get(key);
      if ( !res.second ) {
          try {
             T tmp = boost::lexical_cast<T>(res.first);
          } catch ( boost::bad_lexical_cast const & ) {
             return make_pair( T(), false ); // not convertible
          }
          return make_pair( boost::lexical_cast<T>(res.first), true );
      } else {
          return make_pair( T(), false ); // error condition
      }
   }
}

The implementor of the class must add extra code to forward the errors, and that code gets intermixed with the actual logic of the problem. In C++ this is probably more burdensome than in a language designed for multiple assignments (a,b=4,5) but still, if the logic depends on the possible error (in here calling lexical_cast should only be performed if we have an actual string) then you will have to cache values into variables anyway.

David Rodríguez - dribeas
I'm familiar with exceptions, and the difficulties. "Exceptional C++" by Herb Sutter is a good book. You misquote me - "return values are not exceptions." That said, "config2" looks fairly general for a "valid object and success code vs. default object and error code" pattern.
reechard
I forgot to add a catch to the `lexical_cast` function. If exceptions were thrown away, `lexical_cast` (simple enough) could be implemented with the same value/state pattern, but the code would not be much nicer than what it is with the try/catch, unless the return value of the modified cast was exactly the same as that of the `get_as` method and could thus be forwarded directly.
David Rodríguez - dribeas
In any case, the code becomes much more complex at each new stage, you must add code to control flow and return statements at any possible point of failure.
David Rodríguez - dribeas
+2  A: 

This question is a bit tricky to answer objectively, and opinions on exceptions can differ quite a lot.

But if I were to speculate, I think the primary reason exceptions is not included in Go is because it complicates the compiler and can lead to non-trivial implications when writing libraries. Exceptions is hard to get right, and they prioritized getting something working.

The primary difference between handling errors through return values and exceptions is that exceptions forces the programmer to deal with unusual conditions. You can never have a "silent error" unless you explicitly catch an exception and do nothing in the catch block. On the other hand, you get implicit return points everywhere inside functions which can lead to other types of bugs. This is especially prevalent with C++ where you manage memory explicitly and need to make sure you never lose a pointer to something you have allocated.

Example of dangerous situation in C++:

struct Foo {
    // If B's constructor throws, you leak the A object.
    Foo() : a(new A()), b(new B()) {}
    ~Foo() { delete a; delete b; }

    A *a;
    B *b;
};

Multiple return values makes it easier to implement return value based error handling without having to rely on out arguments to functions, but it doesn't change anything fundamentally.

Some languages have both multiple return values and exceptions (or similar mechanisms). One example is Lua.

Josef Grahn
+2  A: 

It's not Go, but in Lua, multiple return is an extremely common idiom for handling exceptions.

If you had a function like

function divide(top,bottom)
   if bottom == 0 then 
        error("cannot divide by zero")
   else
        return top/bottom
   end
end

Then when bottom was 0, an exception would be raised and the program's execution would halt, unless you wrapped the function divide in a pcall (or protected call).

pcall always returns two values: the first is result is a boolean telling whether the function returned successfully, and the second result is either the return value or the error message.

The following (contrived) Lua snippet shows this in use:

local top, bottom = get_numbers_from_user()
local status, retval = pcall(divide, top, bottom)
if not status then
    show_message(retval)
else
    show_message(top .. " divided by " .. bottom .. " is " .. retval)
end

Of course, you don't have to use pcall, if the function you're calling already returns in the form of status, value_or_error.

Multiple return has been good enough for Lua for several years, so while that doesn't ensure that it's good enough for Go, it is supportive of the idea.

Mark Rushakoff
+1  A: 

Here's an example of how multiple return values might work in c++. I wouldn't write this code myself, but I don't think it is entirely out of the question to use such an approach.

#include <iostream>
#include <fstream>
#include <string>
using namespace std;

// return value type
template <typename T> 
struct RV {
    int mStatus;
    T mValue;

    RV( int status, const T & rv ) 
     : mStatus( status ), mValue( rv ) {}
    int Status() const { return mStatus; }
    const T & Value() const {return mValue; }
};

// example of possible use
RV <string> ReadFirstLine( const string & fname ) {
    ifstream ifs( fname.c_str() );
    string line;
    if ( ! ifs ) {
     return RV <string>( -1, "" );
    }
    else if ( getline( ifs, line ) ) {
     return RV <string>( 0, line );
    }
    else {
     return RV <string>( -2, "" );
    }
}

// in use
int main() {
    RV <string> r = ReadFirstLine( "stuff.txt" );
    if ( r.Status() == 0 ) {
     cout << "Read: " << r.Value() << endl;
    }
    else {
     cout << "Error: " << r.Status() << endl;
    }
}
anon
:) This reminds me of some proprietary code I read a couple of months ago, where they used this type of approach to guarantee that return values were always processed. The class would keep track of whether `Status()` had been called and **throw** and exception (in debug builds) during destruction if it had not been. Exceptions were prohibited anywhere else in the code so there would be no `try/catch` anywhere in the call stack, the exception would kill the application. I wonder how they came to that code...
David Rodríguez - dribeas