Exception idea: third Throwable branch. Probably won't work but written anyway.

Posted 2022-06-27

I'll briefly write up an idea I had for a marginal exception performance improvement migration path and why it probably won't work, though some of the individual items in the way should probably be fixed anyway.

Next week, I'll try to find time to write about inferred attributes, which might also be useful for catch blocks.

Core D Development Statistics

In the community

Community announcements

See more at the announce forum.

Scoped Exception branch

There's been a lot of ideas about @nogc exceptions. Now, I want to be clear: this probably isn't worth pursuing. A quick GC allocation on the exception is a very small total cost of ownership relative to alternatives. I recommend you just use GC and exceptions and stop worrying. That said, let's lay out the idea.

Using non-GC allocations is actually very easy - you can make a static exception, you can malloc and emplace one, and there's the -dip1008 switch.

Each of these do have problems though: static ones can't be reused nor returned from threads. malloc'd ones need to be free'd. And -dip1008 has trouble being returned from threads and trouble linking libraries due to the magic compiler switch's potential incompatibilities. Catching code needs to be aware of these things and that can happen anywhere, so it isn't realistic.

It occurs to me there is a potential way to work around all these: have a new type. Existing code that catches Exception can keep things the way it is; they won't be affected by the new idea, and you aren't supposed to catch Throwable since it includes Error... though the spec is contradictory on that and the implementation, until recently, worked fine if you did, and it makes sense to.... but that's another story anyway lol.

The official party line right now is you should only be catching Exception. So If you made a new branch off Throwable, which the compiler allows, you'd have freedom to define new interfaces for these specialized exceptions and rules for code that catches them without worrying so much about breaking existing code.

There are several complications though:

  • Thread.join returns a Throwable. It catches everything and returns it. This is the key complication bringing pain in TLS exceptions too.
  • nothrow assumes catch(Exception) indeed catches them all:
    class Foo : Throwable {
    	this() { super("foo"); }
    }
    
    void main() nothrow {
    	throw new Foo(); // errors it is nothrow yet might throw
    }

    And yet...

    class Foo : Throwable {
    	this() { super("foo"); }
    }
    
    void main() nothrow {
    	try {
    		throw new Foo();
    	} catch(Exception e) {} // error silenced! yet Foo not caught.
    }
  • catch(scope X) doesn't work at all. This would ideally be fixed so the compiler can help ensure you don't leak something.
  • Related note: catch(immutable X) will still catch mutable exceptions too, so it is also broken.
  • There's no kind of RAII destructor inside the catch block, so if you are supposed to call free, the compiler has no facility to help you remember.
  • You can't force a method to be implemented in all child classes, even if already overridden by the parent class:
    abstract class ScopeException : Throwable {
    	abstract ScopeException dup() const scope;
    }
    
    class MyException : ScopeException {
    	override MyException dup() { ... }
    }
    
    class AnotherChild : MyException {
    	// oops no dup, won't be right runtime type anymore
    	// a `protected` interface with a `public` helper function
    	// that compares typeid might notice it and throw an Error
    	// though.
    }

Generally speaking, catch blocks have a number of weird quirks. These probably could be fixed, but any catch(Throwable), including the ones in druntime, might be broken and we might end up changing nothrow. Even if this is arguably allowed, it is iffy in practice... the more I write up these details, the more I'm pretty sure it doesn't actually solve the backward compatibility problem after all, but let's push that aside and see what we might do.

What we'd want to do is make it scoped, have a clone method so you can copy it out elsewhere if you want to escape it after all, then have the helper function to allocate them in temporary memory; indeed, a static TLS block would probably be ok if it can't be returned or shared anymore. I'd also like to make the exceptions not stringly typed anymore but that's a separate issue to the allocation, just I always think of it together.

With these, you can confidently do things this way without worry that a catch block would do something inappropriately to your new type.

But.... that said, I don't think this idea actually works. I still think you're best off just using the GC, it is by far the safest way, and if you wanted to skip a collection for these, fine; an allocation isn't necessarily bad and if you temporarily leak, an exception should be small and relatively rare anyway, so you can batch... or heck, let the exception do the collection since when it is thrown, you expect it slow anyway!

Oh well.