Every non-trivial Swift function should throw, right?


(John Spurlock) #1

"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

One of the annoying things about checked exceptions in other languages is
that they somewhat dictate client behavior wrt error handling and flow.
Also expensive (for some value of expensive) when they occur, so not a good
choice for standard-case control flow.

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 or a bespoke error callback argument.

i.e. always: func parse(str: String) throws -> Foo
instead of: func parse(str: String) -> Foo? // loss of information: why
did it fail?


(Brent Royal-Gordon) #2

"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

[snip]

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.

···

--
Brent Royal-Gordon
Architechies


(Erica Sadun) #3

I'm hopping in here ridiculously late, but wasn't someone going to propose a vanilla universal stdlib error type along the lines of

struct Error: ErrorType {
   let reason: String
}

(preferably with auto-captured location context (http://ericasadun.com/2015/08/27/capturing-context-swiftlang/) and a custom mutable dictionary.)

-- Erica

···

On Mar 5, 2016, at 5:59 PM, Brent Royal-Gordon via swift-users <swift-users@swift.org> wrote:

"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

[snip]

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.

... etc


(Jordan Rose) #4

There's some more expansion on different kinds of errors and the Swift model both in last year's WWDC presentation <https://developer.apple.com/videos/play/wwdc2015/106/?time=1816> (look for "There are really three different ways that functions can fail") and in the internal docs <https://github.com/apple/swift/blob/master/docs/ErrorHandling.rst> in the Swift repo. (And again with more detail in the original "rationale" doc <https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst>.)

Jordan

···

On Mar 5, 2016, at 16:59, Brent Royal-Gordon via swift-users <swift-users@swift.org> wrote:

"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

[snip]

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.


(Dave Abrahams) #5

I know some people around here really want us to standardize on NSError
for this purpose. If you have good reasons why we shouldn't, now would
be a good time to develop those arguments.

···

on Sat Mar 05 2016, Erica Sadun <swift-users-AT-swift.org> wrote:

I'm hopping in here ridiculously late, but wasn't someone going to
propose a vanilla universal stdlib error type along the lines of

struct Error: ErrorType {
   let reason: String
}

(preferably with auto-captured location context
(http://ericasadun.com/2015/08/27/capturing-context-swiftlang/
<http://ericasadun.com/2015/08/27/capturing-context-swiftlang/>) and a
custom mutable dictionary.)

--
-Dave


(James Campbell) #6

There is a proposal which is aiming to tackle a small subset of these issues

https://github.com/apple/swift-evolution/pull/196

···

*___________________________________*

*James⎥Head of Trolls*

*james@supmenow.com <james@supmenow.com>⎥supmenow.com <http://supmenow.com>*

*Sup*

*Runway East *

*10 Finsbury Square*

*London*

* EC2A 1AF *

On Mon, Mar 7, 2016 at 9:44 PM, Jordan Rose via swift-users < swift-users@swift.org> wrote:

On Mar 5, 2016, at 16:59, Brent Royal-Gordon via swift-users < > swift-users@swift.org> wrote:

"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

[snip]

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.

There's some more expansion on different kinds of errors and the Swift
model both in last year's WWDC presentation
<https://developer.apple.com/videos/play/wwdc2015/106/?time=1816> (look
for "There are really three different ways that functions can fail") and in
the internal docs
<https://github.com/apple/swift/blob/master/docs/ErrorHandling.rst> in
the Swift repo. (And again with more detail in the original "rationale"
doc
<https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst>
.)

Jordan

_______________________________________________
swift-users mailing list
swift-users@swift.org
https://lists.swift.org/mailman/listinfo/swift-users


(John Spurlock) #7

The error handling rationale document [1] is an excellent review, thanks.

If you are going to consider optional returns as appropriate for a
certain class of errors, that class needs to be crystal clear - and
enforceable/hinted-at by the compiler. Java and C# exception
hierarchies have rationale, but end-users don't read documentation.
Since they can only be minimally enforced by the type system, it has
turned out to be a free-for-all.

Too bad Result<T> does not exist! The Result handling flow looks
depressingly familiar (as mentioned above), it's just as tedious and
error-prone to do by hand as C/Go-style error checking, so it's weird
that the current error system does not have first-class support for
them under the same rationale.

Perhaps Optional<T> and Result<T> would be best thought of as
fundamentally related in some way, and have a similar protocol/shape
called Either<T,Or> where Or is nil for optionals and
ErrorType/NSError for results. Or consider optionals an error of a
certain kind for simplicity.

[1] https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst

···

(And again with more detail in the original "rationale" doc.)

Jordan


(Erica Sadun) #8

Wrong list for language discussions, but domain/code/userInfo aren't really Swifty.
It feels like we're tied down to a archaic construct just for the sake of consistency.

An error should answer the following questions:

* What went wrong?
* Where did it go wrong?
* What other information am I passing along about the circumstances of the error?

Which ties into my reason/context/errorInfo over domain/code/userInfo.

-- E, who has replaced -users with -evolution in the reply

···

On Mar 5, 2016, at 11:02 PM, Dave Abrahams via swift-users <swift-users@swift.org> wrote:

on Sat Mar 05 2016, Erica Sadun <swift-users-AT-swift.org> wrote:

I'm hopping in here ridiculously late, but wasn't someone going to
propose a vanilla universal stdlib error type along the lines of

struct Error: ErrorType {
  let reason: String
}

(preferably with auto-captured location context
(http://ericasadun.com/2015/08/27/capturing-context-swiftlang/
<http://ericasadun.com/2015/08/27/capturing-context-swiftlang/>) and a
custom mutable dictionary.)

I know some people around here really want us to standardize on NSError
for this purpose. If you have good reasons why we shouldn't, now would
be a good time to develop those arguments.

--
-Dave

_______________________________________________
swift-users mailing list
swift-users@swift.org
https://lists.swift.org/mailman/listinfo/swift-users