Important note: the choice of function name and consequent manipulations are incredibly creepy without context, which you can find here.

Here's a function. According to its docs, the returned string holds Jester's last words.

string KillJester(string word, int count, bool? dryRun);

I find it useful to think about the domains and ranges of functions in C#, though I admit this kind of analysis has limited use in a language which cares little about side effects. For anyone not in the know, a function domain defines the set of inputs for which the function is defined. What constitutes 'defined' in C# is anyone's guess, but I'm going to assert that KillJester is defined for all possible values of its inputs.

The variable dryRun is either true, false, or null. The possible values for count are anywhere between -2,147,483,648 and 2,147,483,647 inclusive.

The possible values a string can hold are less obvious. Limitations in the language itself prevent programs declaring strings longer than about a billion characters in length, but we needn't bother with such practicalities. For our purposes, a string is simply a list of chars, where said list has length 0 or more, and each value is between U+0000 and U+FFFF inclusive.

In other words, the domain of KillJester is infinite (theoretically), but I can express it clearly: KillJester is defined for any tuple containing any list of chars (0 or more), any number between  and one of true, false, and null. A possible input: ("verisimilitude", 12, false).

What about its range? That's harder to answer. It could be null (but I'm hoping that whoever wrote this function would've made it return string? if that were possible. It might return, as we stated above, a list of chars of some length, maybe 0, maybe enough to run out of memory. But it might 'return' an exception. If it does, it won't store that exception in a variable, but will instead go on a hunt for a try block, but let's ignore that for now. The type of the exception is anyone's guess — in practical terms I can limit myself to exceptions defined in the set of imported dependencies I've imported, but that doesn't help us in the general case. All I can tell is that the type returned is a subclass of Exception.

That's a pretty big range — obviously infinite, but now open to an infinite types as well as infinite length arrays of chars. Of these extra types, some might be exceptions that we definitely don't want to catch, like OutOfMemoryException, StackOverflowException, and (for anyone familiar) MoqException (call them 'panicky' exceptions) — but others are exceptions we're meant to understand in the context of the program because we might be able to recover from them. We can't tell which is which except by trust, luck, or very good documentation. Absolute madness.


Sensible developers would give up now and try a different language, but we can attempt to make this easier for people wishing to kill Jester. Clearly it isn't useful to include panicky exceptions in the range of the function because (not to put too fine a point on it) they're exceptions. However, there are other exceptions that should be understood in the flow of the program, and they should be semantically separated from panicky ones. In the case of KillJester, we can imagine throwing a JesterMadeItException (Jester, burdened in this program with agency, might survive regardless of our inputs, so this is an exception even I'm happy with). The easiest way to separate this exception out is to embed it into the return type, creating a wrapper type for the string.

What does that look like? Here's an option.

IKillJesterResult KillJester(string word, int count, bool? dryRun);
interface IKillJesterResult {
  bool IsDead { get; }
  string LastWords { get; }
}

class KilledJesterResult {
  public KilledJesterResult(string lastWords) => LastWords = lastWords;
  public bool IsDead => true;
  public string LastWords { get; }
}

class JesterMadeItResult {
  public bool IsDead => false;
  public string LastWords => throw new InvalidOperationException();
}

The interface looks good at first (though the joke's wearing awfully thin) but we're now assuming that developers will definitely only access LastWords after confirming that they exist by checking IsDead. I think we can do one better.

interface IKillJesterResult {
  T Handle<T>(Func<string, T> handleWords, Func<T> handleMadeIt);
  void Handle(Action<string> handleWords, Action handleMadeIt);
}

class KilledJesterResult {
  private string _lastWords;
  public KilledJesterResult(string lastWords) =>
    _lastWords = lastWords;
    
  public T Handle<T>(Func<string, T> handleWords, Func<T> _) =>
    handleWords(_lastWords);
  public void Handle(Action<string> handleWords, Action _) =>
    handleWords(_lastWords);
}

class JesterMadeItResult {
  public T Handle<T>(Func<string, T> _, Func<T> handleMadeIt) =>
    handleMadeIt();
  public void Handle(Action<string> _, Action handleMadeIt) =>
    handleMadeIt();
}

On the one hand, I totally understand why void isn't a return type, and so Action needs to supplement Func. On the other hand, I've ended up duplicating a lot of code because of this.

This perhaps looks much more complicated than it should, but anyone making use of the function now knows exactly how to handle its return result, and doesn't need to bother with a try-catch block — any exceptions KillJester now throws are just that.

The code is still terrible though. We haven't covered cases where you want to crash if Jester made it. We've required that any logging or metrics work be done inside the actions we pass into the result class. We'll set to work improving these a couple of posts from now.


In the first part of this series I looked at strengthening the types of input variables to cut out the need for input sanitation exceptions. By strengthening the function's return types, we've now cut out the subset of exceptions that are deliberately thrown by the program in response to non-standard events. Here, Jester made it, but this example is equivalent to http timeouts, keys not found in dictionaries, values not found in lists, and so on. This means the only exceptions left are those we would actually consider panics, and, like panics, we should allow them to bubble all the way to the top.