A Game of Throws
In Infinite Laziness, we saw how the
yield keyword lets us work with lazy evaluation, in which work is deferred and performed on demand once something else actually needs to consume the result. Lazy evaluation with
yield isn't free, though; it has some unfortunate consequences.
This week, we'll see how lazy evaluation affects exception handling. Next week, we'll see how
yield complects brevity with laziness, resulting in some undesirable patterns.
Let's say you're lazily producing a series of objects, but the creation of each object has a real potential to throw an exception. The developer calling the lazy method knows that the operation could fail, and attempts to protect the rest of the system:
Despite wrapping the dangerous method call with a
try/catch block, callers of SafelyBuildGrenades are in for a surprise. The call to BuildGrenades will never throw an ExplosionException, while callers of SafelyBuildGrenades might set off a grenade just by looking at them!
When you call a
yielding method, no work is performed and the call returns immediately. It returns an object that knows how to produce the list of items on demand. Those items won't be produced until someone starts looping through them. In our example above, this quickly-made object is likewise immediately returned by SafelyBuildGrenades. The consumer of the result of SafelyBuildGrenades is then free to start looping through it, at which point the code in BuildGrenades will start to execute. Since an ExplosionException could only be thrown way up in that consumer's loop, the consumer's loop is where the exception will start moving up the call stack.
yieldis an exception smuggler.
yieldhides your dangerous code in a harmless-looking package in order to sneak it past unsuspecting
try/catchblocks. The harmful contents can then be unpacked by the recipient.
When using lazy evaluation, which includes the now-ubiquitous IEnumerable extension methods (Select, Where, Take, Skip...), take care. Work won't be performed until you begin to iterate through the results, so your exception handling will need to take place higher up the call stack.