[swift-users] Every non-trivial Swift function should throw, right?
brent at architechies.com
Sat Mar 5 18:59:30 CST 2016
> "so pretty much every non-trivial #swift function should throw, right? cheap & gives caller choice to catch, rethrow, try? or try! (4 in 1)"
> -- https://twitter.com/johnspurlock/status/704478619779866625
> Given that Swift provides multiple language-standard ways for clients to deal with a function marked as 'throws', it seems like almost all non-trivial shared functions should provide the additional information of the error in that standard form, instead of hiding it behind an optional return type
No, I don't think so.
First of all, there are functions which I can't imagine describing as trivial, but which nonetheless cannot fail except by programmer error. For instance, sorting can only fail if you provide a comparator which doesn't work properly (by, for instance, saying that both `a < b` and `b < a` are true). There is no error reporting needed at all for sorting, because the only possible errors are outright mistakes by the programmer. Those should be handled with preconditions, and the function itself should not signal the possibility of an error in any way at all.
Secondly, there are functions which can only fail in a single, obvious way. For instance, the `indexOf(_:)` method can only fail by reaching the end of the Collection without finding an element matching its parameter. It could indicate this by throwing a CollectionError.NoMatch error, but that would be overkill; it's far easier to return an optional Int, with `nil` meaning "no match".
Of course, the line between what should be optional and what should be a thrown error is necessarily subjective. Should `Int.init(_: String, radix: Int = 10)` be throwing or optional? On the one hand, all possible errors boil down to one: "you passed a string that isn't a number". On the other, in some contexts it might be helpful to know the problem is "there's a space at character offset X".
But the solution to this tension cannot and should not simply be "always use the most heavyweight error handling mechanism available". That is the answer many languages offer, and we've all seen where it leads.
Here are my rules of thumb:
• If the error should never happen unless the programmer makes a mistake, use a precondition.
• If there is only one way the error can be caused (or there is rarely any useful way for callers to respond to different causes differently), AND error conditions are so common that the error code paths ought to be given just as much weight as the success code paths, use an optional (or a boolean).
• For everything else, use thrown errors. That is, errors which are neither impossible in well-written code, nor so common as to be equally likely as successes *and* without useful distinctions from one another, should be thrown.
These rules are not purely mechanical; they require judgement from the API's designers and embed opinions about uses which may inconvenience some callers. But there's simply no way around that—the best APIs are almost always opinionated ones.
> or a bespoke error callback argument.
This is worth discussing separately.
I assume that you mean passing a closure to handle either success or failure. That's usually only done with asynchronous operations, which by necessity *must* communicate their result through a callback, so neither returning an optional nor throwing an error is available.
However, we have conventional equivalents of both, which are used in the same cases. The equivalent of returning an optional/boolean is passing a single optional/boolean to the completion, and the equivalent of throwing is passing both an optional/boolean and an optional error to the completion. The throwing equivalent could be expressed a little more cleanly if we had a Result/Either type in the standard library, but we currently don't, so we can't.
Some developers prefer to pass separate success and failure closures. I've never been a fan of this approach except when writing functional-style APIs on a type like Result, where you can use it to arbitrarily chain operations together. Otherwise I think it fights the language—for instance, it's not compatible with trailing closure syntax.
I have high hopes that a future version of Swift will either formalize the success/failure pattern in callbacks, or provide some way to avoid having to write callbacks explicitly, just as Swift 2 formalized the old "return optional and have an error out parameter" pattern into the current throwing system. But we're not there yet and we won't be until at least Swift 4, so until then, we'll have to make do with awkward multiple-parameter patterns.
More information about the swift-users