nothrow by default

Johannes Pfau nospam at
Sun Jan 5 10:32:23 UTC 2020

Am Sat, 04 Jan 2020 13:38:53 -0800 schrieb Walter Bright:

> The short rationale is that exceptions being a "pay only if you use
> them" is a complete fraud. They're expensive to support, meaning
> performance programs use other ways of signalling errors and use
> nothrow.

I totally agree to that (memory overhead, TypeInfo, support code). In 
addition, this overhead means that exceptions are unlikely to be supported 
in embedded systems, which will lead to a language ecosystem split (this 
effectively happened to C++).

But I'm not sure if that's a good rationale for nothrow by default. We 
already tell users to use exceptions only for "exceptional cases" and use 
other error handling mechanism for common error paths. With this change, 
we'd force users even stronger to use other error handling methods. But 
those don't have language support and we're back to C times, where users 
forget to check error codes, ...

Herb Sutter came to the same conclusion recently and proposed an 
alternative exception implementation for C++. If you didn't see this 
already Walter, it well worth to have a look:

I'll summarize the idea here, as far as I remember and adapted to D 
terminology / context:

Rationale: There are two common ways to handle errors: Backtrace/
exception based and error code based. Both have drawbacks (exceptions: 
performance, memory / implementation overhead, not supported in embedded 
systems) (error codes: not "bubbling up" the call stack automatically, no 
way to force user to check, so might accidentially ignore errors).

1) Implement exceptions like return codes: Each function which can throw
   returns a union ValueOrError{T result; Error err}. The discriminator
   bit is stored as a CPU flag register which is not  used across
   function calls (e.g. the OVERFLOW status bit). Error is a two-word 
   error code: Error {size_t code; size_t context}. This explains the
   ABI, for now nothing of this is user-visible.

   Whenever a call to a function which might throw occurs and there's a 
   try-catch handler, insert this code after the call:
   if (OVERFLOW == 1)
       goto catch_handler;
   If there's no catch handler installed and a function might throw:
   if (OVERFLOW == 1)
   The return value is already in the return registers. Therefore
   propagating error codes upwards is cheap.

   The benefits now are simple: Implementation code / memory overhead is
   almost zero, no TypeInfo required, trivial to implement on embedded 
   systems, .... At the same time, exceptions bubble up properly so they
   can't be accidentially unhandled. The runtime overhead in the success 
   case is obviously higher than for some exception systems, but it's
   only a simple conditional jump. It is cheaper than manual error codes,
   as we reuse the same registers for return value and error code, 
   benefitting register allocation. Error propagation also uses the same 
   registers in all functions and is therefore very efficient.

   For D / legacy exception support, you just store  
   code=LEGACY_EXCEPTION, context=Exception pointer. To allocate the 
   error codes in a distributed way and to check 
   for different error types, we can simply store dummy values in the 
   data segment to get unique addresses:
   ubyte OutOfMemoryError; ... ; if (ret.code == &OutOfMemoryError)...

2) (Optional): Herb arguest that because of throw ... it is easy to spot 
   where an exception originates, but it's more difficult to find where 
   an exception was propagated. As a solution, 
   whenever calling a throws function, the calls should be preceeded by
   auto value = throw myThrowingFunction();
   Here throw does essentially this: If myThrowingFunction threw, rethrow
   the exception. Otherwise return the return value.

3) (C++ specific): Error codes instead of exceptions, "Type based Errors":
   I don't really know what is meant by this "Type-Based Errors" 
   terminology, seems to be a C++ marketing thing ("we do everything with 
   types now")... The important takeaway is to not allocate complex 
    exception objects. Use the error code and context value. If really 
   more context than one word is necessary, it's still possible to stuff 
   a pointer into context.

4) (D-specific): Not mentioned in the C++ proposal, but some interesting 
   extension: A more local error handling solution:
   In D we have try/catch if we want to handle errors from multiple  
   functions calls and scope(failure) to handle all errors in a function. 
   But fine-grain error handling is annoying:
   catch(Exception e)
           case DNS_ERROR:
               writeln("Dns erorr");
           case ...
   // success code here

   quite some overhead here. Traditional error codes are better here:
   T ret;
   switch(ret = foo())
       case ...:
   // Success here

   Maybe we could formalize this: We can expose the ValueOrError union to 
   the user. foo() then returns ValueOrError!T and the 'throw' in 2) 
   could simply check for errors, otherwise return ValueOrError!(T).value.
   We could then add an opCast(bool) overload to ValueOrError to make
   if (value) work, add a function to check/abort and convert 
   ValueOrError.getValue and overload the switch statement on this:
   auto res = foo();
        case DNS_ERROR:
        default: // Other error
   // With flow-typing we'd know that ValueOrError is no error here.   
   Without, we have to explicitly convert the type somehow:

   T value;
   // Switch magically overloaded on error / value tuple
   switch ((value, error) = foo())

   However, with these ideas in 4), we'd have to force the user somehow 
   to check the error bit and make the value only accessible after the 

* I have not thought about how exception chaining fits into all this.
* These exceptions do not naturally propagate through foreign language
  interfaces, although I think we don't guarantee this in D right now

If there's any real interest in this, I'd be happy to write a proper 
DIP(s) for it (I think 1, 2 and 4 are all independent DIPs). If C++ 
really gets this, we might have to support it anyway for C++ interop.

PS: Back to the original question: nothrow by default: With 2) every 
function which might throw and where errors are not handled locally needs 
to have throw in front of the function call. Obviously, we don't want to 
have too many of these, so nothrow functions should be the common case. 
And the common case should be the default, so nothrow by default also 
makes sense in this context.


More information about the Digitalmars-d mailing list