[Proposal Idea] catching functions for composable and cps error handling


(Matthew Johnson) #1

I came up with an interesting idea last night that might be a natural enhancement to Swift’s current error handling. I’m curious to see whether others think it is worth pursuing or not.

There are two motivating examples for this. It could provide the obvious way to flow errors through code written in continuation passing style (whether asynchronous or not). It could also provide a way to abstract over common error handling code.

The basic idea is to allow catching functions that would be a complement to throwing functions. Catching functions would have one or more catch blocks at the top level and could also accept arguments:

func handler(int i: Int, str: String) catches {
  // i and str are in scope
} catch VendingMachineError.InvalidSelection {
  // i and str are not in scope
} catch VendingMachineError.OutOfStock {
  // i and str are not in scope
}

The function could be called as normal:
handler(int: 1, str: “”)

In this case the body would be executed.

The function could also be called with a “catch” clause:
handler(catch VendingMachineError.InvalidSelection)

In this case the top level catch clauses would be evaluated as if they part of a do-catch statement in which an error was thrown.

Note that there is no colon after the `catch` in the function call. This would avoid conflicting with a potential argument named catch and would also call attention to the fact that it is not a normal argument. I don’t think `throw` would be appropriate here as that could be ambiguous if the function containing the call to `handler` was a throwing function and also because an error would not be thrown up the stack, but rather caught directly by `handler`.

It may be worthwhile to consider requiring a catching function to handle all errors if it wishes to return a value so that it is able to return a value when catching an error no matter what the error is.

Alternatively, (and maybe more interesting) since the compiler knows at the call site whether the function was provided regular arguments or an error to catch these two cases could be handled independently, with the body returning a value and the result of a “catching” call returning a value indicating whether the error was handled or not (or something similar).

Here is how this would look when it is applied to the motivating examples.

First is a cps example using a catching closure.

// @exhaustive requires the catching function to catch all errors
func cps(then: Int -> () @exhaustive catches) {
  if (checkSomeState) {
    then(42)
  } else {
    then(catch VendingMachineError.InvalidSelection)
  }
}

cps() { i: Int in
  // do some work using i
} catch VendingMachineError.InvalidSelection {
  // handle the error, i is not in scope
} catch VendingMachineError.OutOfStock {
  // handle the error, i is not in scope
} catch {
  // handle all other errors
}

Second is an example showing how this could be used to abstract over error handling logic:

func handler() catches {
catch VendingMachineError.InvalidSelection {
  // some common code handling InvalidSelection
catch VendingMachineError.OutOfStock {
  // some common code handling OutOfStock
// handle some other cases as well
}

func doSomething() {

  do {

    try someThrowingFunction()

  } catch handler { // compiler inserts call: handler(catch errorThatWasThrown)

    // not sure if a body would make sense here
    // if it does it would only be executed when handler actually handled the error

  // we only proceed to the next case if handler did not handle the error
  } catch VendingMachineError.InsufficientFunds(let coinsNeeded) {

  // we can provide arguments to the error handling logic by calling a function that returns a catching closure
  } catch someFunctionReturningAClosureThatCatches(arg: someValueDeterminingHowToHandleErrors)
  }

}