mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
741 lines
29 KiB
ReStructuredText
741 lines
29 KiB
ReStructuredText
Error Handling in Swift 2.0
|
|
===========================
|
|
|
|
As a tentpole feature for Swift 2.0, we are introducing a new
|
|
first-class error handling model. This feature provides standardized
|
|
syntax and language affordances for throwing, propagating, catching,
|
|
and manipulating recoverable error conditions.
|
|
|
|
Error handling is a well-trod path, with many different approaches in
|
|
other languages, many of them problematic in various ways. We believe
|
|
that our approach provides an elegant solution, drawing on the lessons
|
|
we've learned from other languages and fixing or avoiding some of the
|
|
pitfalls. The result is expressive and concise while still feeling
|
|
explicit, safe, and familiar; and we believe it will work beautifully
|
|
with the Cocoa APIs.
|
|
|
|
We're intentionally not using the term "exception handling", which
|
|
carries a lot of connotations from its use in other languages. Our
|
|
proposal has some similarities to the exceptions systems in those
|
|
languages, but it also has a lot of important differences.
|
|
|
|
Kinds of Error
|
|
--------------
|
|
|
|
What exactly is an "error"? There are many possible error conditions,
|
|
and they don't all make sense to handle in exactly the same way,
|
|
because they arise in different circumstances and programmers have to
|
|
react to them differently.
|
|
|
|
We can break errors down into four categories, in increasing order of
|
|
severity:
|
|
|
|
A **simple domain error** arises from an operation that can fail in
|
|
some obvious way and which is often invoked speculatively. Parsing an
|
|
integer from a string is a really good example. The client doesn't
|
|
need a detailed description of the error and will usually want to
|
|
handle the error immediately. These errors are already well-modeled
|
|
by returning an optional value; we don't need a more complex language
|
|
solution for them.
|
|
|
|
A **recoverable error** arises from an operation which can fail in
|
|
complex ways, but whose errors can be reasonably anticipated in
|
|
advance. Examples including opening a file or reading from a network
|
|
connection. These are the kinds of errors that Apple's APIs use
|
|
NSError for today, but there are close analogues in many other APIs,
|
|
such as ``errno`` in POSIX.
|
|
|
|
Ignoring this kind of error is usually a bad idea, and it can even be
|
|
dangerous (e.g. by introducing a security hole). Developers should be
|
|
strongly encouraged to write code that handles the error. It's common
|
|
for developers to want to handle errors from different operations in
|
|
the same basic way, either by reporting the error to the user or
|
|
passing the error back to their own clients.
|
|
|
|
These errors will be the focus of this proposal.
|
|
|
|
The final two classes of error are outside the scope of this proposal.
|
|
A **universal error** is theoretically recoverable, but by its nature
|
|
the language can't help the programmer anticipate where it will come
|
|
from. A **logic failure** arises from a programmer mistake and should
|
|
not be recoverable at all. In our system, these kinds of errors are
|
|
reported either with Objective-C/C++ exceptions or simply by
|
|
logging a message and calling ``abort()``. Both kinds of error are
|
|
discussed extensively in the rationale. Having considered them
|
|
carefully, we believe that we can address them in a later release
|
|
without significant harm.
|
|
|
|
Aspects of the Design
|
|
---------------------
|
|
|
|
This approach proposed here is very similar to the error handling
|
|
model manually implemented in Objective-C with the ``NSError``
|
|
convention. Notably, the approach preserves these advantages of this
|
|
convention:
|
|
|
|
- Whether a method produces an error (or not) is an explicit part of
|
|
its API contract.
|
|
|
|
- Methods default to *not* producing errors unless they are explicitly
|
|
marked.
|
|
|
|
- The control flow within a function is still mostly explicit: a
|
|
maintainer can tell exactly which statements can produce an error,
|
|
and a simple inspection reveals how the function reacts to the
|
|
error.
|
|
|
|
- Throwing an error provides similar performance to allocating an
|
|
error and returning it -- it isn't an expensive, table-based stack
|
|
unwinding process.
|
|
|
|
- Cocoa APIs using standard ``NSError`` patterns can be imported into
|
|
this world automatically. Other common patterns (e.g. ``CFError``,
|
|
``errno``) can be added to the model in future versions of Swift.
|
|
|
|
In addition, we feel that this design improves on Objective-C's error
|
|
handling approach in a number of ways:
|
|
|
|
- It eliminates a lot of boilerplate control-flow code for propagating
|
|
errors.
|
|
|
|
- The syntax for error handling will feel familiar to people used to
|
|
exception handling in other languages.
|
|
|
|
- Defining custom error types is simple and ties in elegantly with
|
|
Swift enums.
|
|
|
|
As to basic syntax, we decided to stick with the familiar language of
|
|
exception handling. We considered intentionally using different terms
|
|
(like ``raise`` / ``handle``) to try to distinguish our approach from
|
|
other languages. However, by and large, error propagation in this
|
|
proposal works like it does in exception handling, and people are
|
|
inevitably going to make the connection. Given that, we couldn't find
|
|
a compelling reason to deviate from the ``throw`` / ``catch`` legacy.
|
|
|
|
This document just contains the basic proposal and will be very
|
|
light on rationale. We considered many different languages and
|
|
programming environments as part of making this proposal, and there's
|
|
an extensive discussion of them in the separate rationale document.
|
|
For example, that document explains why we don't simply allow all
|
|
functions to throw, why we don't propagate errors using simply an
|
|
``ErrorOr<T>`` return type, and why we don't just make error propagation
|
|
part of a general monad feature. We encourage you to read that
|
|
rationale if you're interested in understanding why we made the
|
|
decisions we did.
|
|
|
|
With that out of the way, let's get to the details of the proposal.
|
|
|
|
Typed propagation
|
|
-----------------
|
|
|
|
Whether a function can throw is part of its type. This applies to all
|
|
functions, whether they're global functions, methods, or closures.
|
|
|
|
By default, a function cannot throw. The compiler statically enforces
|
|
this: anything the function does which can throw must appear in a
|
|
context which handles all errors.
|
|
|
|
A function can be declared to throw by writing ``throws`` on the
|
|
function declaration or type::
|
|
|
|
func foo() -> Int { // This function is not permitted to throw.
|
|
func bar() throws -> Int { // This function is permitted to throw.
|
|
|
|
``throws`` is written before the arrow to give a sensible and consistent
|
|
grammar for function types and implicit ``()`` result types, e.g.::
|
|
|
|
func baz() throws {
|
|
|
|
// Takes a 'callback' function that can throw.
|
|
// 'fred' itself can also throw.
|
|
func fred(_ callback: (UInt8) throws -> ()) throws {
|
|
|
|
// These are distinct types.
|
|
let a : () -> () -> ()
|
|
let b : () throws -> () -> ()
|
|
let c : () -> () throws -> ()
|
|
let d : () throws -> () throws -> ()
|
|
|
|
For curried functions, ``throws`` only applies to the innermost
|
|
function. This function has type ``(Int) -> (Int) throws -> Int``::
|
|
|
|
func jerry(_ i: Int)(j: Int) throws -> Int {
|
|
|
|
``throws`` is tracked as part of the type system: a function value
|
|
must also declare whether it can throw. Functions that cannot throw
|
|
are a subtype of functions that can, so you can use a function that
|
|
can't throw anywhere you could use a function that can::
|
|
|
|
func rachel() -> Int { return 12 }
|
|
func donna(_ generator: () throws -> Int) -> Int { ... }
|
|
|
|
donna(rachel)
|
|
|
|
The reverse is not true, since the caller would not be prepared to
|
|
handle the error.
|
|
|
|
A call to a function which can throw within a context that is not
|
|
allowed to throw is rejected by the compiler.
|
|
|
|
It isn't possible to overload functions solely based on whether the
|
|
functions throw. That is, this is not legal::
|
|
|
|
func foo() {
|
|
func foo() throws {
|
|
|
|
A throwing method cannot override a non-throwing method or satisfy a
|
|
non-throwing protocol requirement. However, a non-throwing method can
|
|
override a throwing method or satisfy a throwing protocol requirement.
|
|
|
|
It is valuable to be able to overload higher-order functions based on
|
|
whether an argument function throws, so this is allowed::
|
|
|
|
func foo(_ callback: () throws -> Bool) {
|
|
func foo(_ callback: () -> Bool) {
|
|
|
|
``rethrows``
|
|
~~~~~~~~~~~~
|
|
|
|
Functions which take a throwing function argument (including as an
|
|
autoclosure) can be marked as ``rethrows``::
|
|
|
|
extension Array {
|
|
func map<U>(_ fn: ElementType throws -> U) rethrows -> [U]
|
|
}
|
|
|
|
It is an error if a function declared ``rethrows`` does not include a
|
|
throwing function in at least one of its parameter clauses.
|
|
|
|
``rethrows`` is identical to ``throws``, except that the function
|
|
promises to only throw if one of its argument functions throws.
|
|
|
|
More formally, a function is *rethrowing-only* for a function *f* if:
|
|
|
|
- it is a throwing function parameter of *f*,
|
|
|
|
- it is a non-throwing function, or
|
|
|
|
- it is implemented within *f* (i.e. it is either *f* or a function or
|
|
closure defined therein) and it does not throw except by either:
|
|
|
|
- calling a function that is rethrowing-only for *f* or
|
|
|
|
- calling a function that is ``rethrows``, passing only functions
|
|
that are rethrowing-only for *f*.
|
|
|
|
It is an error if a ``rethrows`` function is not rethrowing-only for
|
|
itself.
|
|
|
|
A ``rethrows`` function is considered to be a throwing function.
|
|
However, a direct call to a ``rethrows`` function is considered to not
|
|
throw if it is fully applied and none of the function arguments can
|
|
throw. For example::
|
|
|
|
// This call to map is considered not to throw because its
|
|
// argument function does not throw.
|
|
let absolutePaths = paths.map { "/" + $0 }
|
|
|
|
// This call to map is considered to throw because its
|
|
// argument function does throw.
|
|
let streams = try absolutePaths.map { try InputStream(filename: $0) }
|
|
|
|
For now, ``rethrows`` is a property of declared functions, not of
|
|
function values. Binding a variable (even a constant) to a function
|
|
loses the information that the function was ``rethrows``, and calls to
|
|
it will use the normal rules, meaning that they will be considered to
|
|
throw regardless of whether a non-throwing function is passed.
|
|
|
|
For the purposes of override and conformance checking, ``rethrows``
|
|
lies between ``throws`` and non-``throws``. That is, an ordinary
|
|
throwing method cannot override a ``rethrows`` method, which cannot
|
|
override a non-throwing method; but an ordinary throwing method can be
|
|
overridden by a ``rethrows`` method, which can be overridden by a
|
|
non-throwing method. Equivalent rules apply for protocol conformance.
|
|
|
|
Throwing an error
|
|
-----------------
|
|
|
|
The ``throw`` statement begins the propagation of an error. It always
|
|
takes an argument, which can be any value that conforms to the
|
|
``Error`` protocol (described below).
|
|
|
|
::
|
|
|
|
if timeElapsed > timeThreshold {
|
|
throw HomeworkError.Overworked
|
|
}
|
|
|
|
throw NSError(domain: "whatever", code: 42, userInfo: nil)
|
|
|
|
As mentioned above, attempting to throw an error out of a function not
|
|
marked ``throws`` is a static compiler error.
|
|
|
|
Catching errors
|
|
---------------
|
|
|
|
A ``catch`` clause includes an optional pattern that matches the
|
|
error. This pattern can use any of the standard pattern-matching
|
|
tools provided by ``switch`` statements in Swift, including boolean
|
|
``where`` conditions. The pattern can be omitted; if so, a ``where``
|
|
condition is still permitted. If the pattern is omitted, or if it
|
|
does not bind a different name to the error, the name ``error`` is
|
|
automatically bound to the error as if with a ``let`` pattern.
|
|
|
|
The ``try`` keyword is used for other purposes which it seems to fit far
|
|
better (see below), so ``catch`` clauses are instead attached to a
|
|
generalized ``do`` statement::
|
|
|
|
// Simple do statement (without a trailing while condition),
|
|
// just provides a scope for variables defined inside of it.
|
|
do {
|
|
let x = foo()
|
|
}
|
|
|
|
// do statement with two catch clauses.
|
|
do {
|
|
...
|
|
|
|
} catch HomeworkError.Overworked {
|
|
// a conditionally-executed catch clause
|
|
|
|
} catch _ {
|
|
// a catch-all clause.
|
|
}
|
|
|
|
As with ``switch`` statements, Swift makes an effort to understand
|
|
whether catch clauses are exhaustive. If it can determine it is, then
|
|
the compiler considers the error to be handled. If not, the error
|
|
automatically propagates out of scope, either to a lexically
|
|
enclosing ``catch`` clause or out of the containing function (which must
|
|
be marked ``throws``).
|
|
|
|
We expect to refine the ``catch`` syntax with usage experience.
|
|
|
|
``Error``
|
|
-----------------
|
|
|
|
The Swift standard library will provide ``Error``, a protocol with
|
|
a very small interface (which is not described in this proposal). The
|
|
standard pattern should be to define the conformance of an ``enum`` to
|
|
the type::
|
|
|
|
enum HomeworkError : Error {
|
|
case Overworked
|
|
case Impossible
|
|
case EatenByCat(Cat)
|
|
case StopStressingMeWithYourRules
|
|
}
|
|
|
|
The ``enum`` provides a namespace of errors, a list of possible errors
|
|
within that namespace, and optional values to attach to each option.
|
|
|
|
Note that this corresponds very cleanly to the ``NSError`` model of an
|
|
error domain, an error code, and optional user data. We expect to
|
|
import system error domains as enums that follow this approach and
|
|
implement ``Error``. ``NSError`` and ``CFError`` themselves will also
|
|
conform to ``Error``.
|
|
|
|
The physical representation (still being nailed down) will make it
|
|
efficient to embed an ``NSError`` as an ``Error`` and vice-versa. It
|
|
should be possible to turn an arbitrary Swift ``enum`` that conforms to
|
|
``Error`` into an ``NSError`` by using the qualified type name as the
|
|
domain key, the enumerator as the error code, and turning the payload
|
|
into user data.
|
|
|
|
Automatic, marked, propagation of errors
|
|
----------------------------------------
|
|
|
|
Once an error is thrown, Swift will automatically propagate it out of
|
|
scopes (that permit it), rather than relying on the programmer to
|
|
manually check for errors and do their own control flow. This is just
|
|
a lot less boilerplate for common error handling tasks. However,
|
|
doing this naively would introduce a lot of implicit control flow,
|
|
which makes it difficult to reason about the function's behavior.
|
|
This is a serious maintenance problem and has traditionally been a
|
|
considerable source of bugs in languages that heavily use exceptions.
|
|
|
|
Therefore, while Swift automatically propagates errors, it requires
|
|
that statements and expressions that can implicitly throw be marked
|
|
with the ``try`` keyword. For example::
|
|
|
|
func readStuff() throws {
|
|
// loadFile can throw an error. If so, it propagates out of readStuff.
|
|
try loadFile("mystuff.txt")
|
|
|
|
// This is a semantic error; the 'try' keyword is required
|
|
// to indicate that it can throw.
|
|
var y = stream.readFloat()
|
|
|
|
// This is okay; the try covers the entire statement.
|
|
try y += stream.readFloat()
|
|
|
|
// This try applies to readBool().
|
|
if try stream.readBool() {
|
|
// This try applies to both of these calls.
|
|
let x = try stream.readInt() + stream.readInt()
|
|
}
|
|
|
|
if let err = stream.getOutOfBandError() {
|
|
// Of course, the programmer doesn't have to mark explicit throws.
|
|
throw err
|
|
}
|
|
}
|
|
|
|
Developers can choose to "scope" the ``try`` very tightly by writing it
|
|
within parentheses or on a specific argument or list element::
|
|
|
|
// Ok.
|
|
let x = (try stream.readInt()) + (try stream.readInt())
|
|
|
|
// Semantic error: the try only covers the parenthesized expression.
|
|
let x2 = (try stream.readInt()) + stream.readInt()
|
|
|
|
// The try applies to the first array element. Of course, the
|
|
// developer could cover the entire array by writing the try outside.
|
|
let array = [ try foo(), bar(), baz() ]
|
|
|
|
Some developers may wish to do this to make the specific throwing
|
|
calls very clear. Other developers may be content with knowing that
|
|
something within a statement can throw. The compiler's fixit hints will
|
|
guide developers towards inserting a single ``try`` that covers the entire
|
|
statement. This could potentially be controlled someday by a coding
|
|
style flag passed to the compiler.
|
|
|
|
``try!``
|
|
~~~~~~~~
|
|
|
|
To concisely indicate that a call is known to not actually throw at
|
|
runtime, ``try`` can be decorated with ``!``, turning the error check
|
|
into a runtime assertion that the call does not throw.
|
|
|
|
For the purposes of checking that all errors are handled, a ``try!``
|
|
expression is considered to handle any error originating from within
|
|
its operand.
|
|
|
|
``try!`` is otherwise exactly like ``try``: it can appear in exactly
|
|
the same positions and doesn't affect the type of an expression.
|
|
|
|
Manual propagation and manipulation of errors
|
|
---------------------------------------------
|
|
|
|
Taking control over the propagation of errors is important for some
|
|
advanced use cases (e.g. transporting an error result across threads
|
|
when synchronizing a future) and can be more convenient or natural for
|
|
specific use cases (e.g. handling a specific call differently within a
|
|
context that otherwise allows propagation).
|
|
|
|
As such, the Swift standard library should provide a standard
|
|
Rust-like ``Result<T>`` enum, along with API for working with it,
|
|
e.g.:
|
|
|
|
- A function to evaluate an error-producing closure and capture the
|
|
result as a ``Result<T>``.
|
|
|
|
- A function to unpack a ``Result<T>`` by either returning its
|
|
value or propagating the error in the current context.
|
|
|
|
This is something that composes on top of the basic model, but that
|
|
has not been designed yet and details aren't included in this
|
|
proposal.
|
|
|
|
The name ``Result<T>`` is a stand-in and needs to be designed and
|
|
reviewed, as well as the basic operations on the type.
|
|
|
|
``defer``
|
|
---------
|
|
|
|
Swift should provide a ``defer`` statement that sets up an *ad hoc*
|
|
clean-up action to be run when the current scope is exited. This
|
|
replicates the functionality of a Java-style ``finally``, but more
|
|
cleanly and with less nesting.
|
|
|
|
This is an important tool for ensuring that explicitly-managed
|
|
resources are released on all paths. Examples include closing a
|
|
network connection and freeing memory that was manually allocated. It
|
|
is convenient for all kinds of error-handling, even manual propagation
|
|
and simple domain errors, but is especially nice with automatic
|
|
propagation. It is also a crucial part of our long-term vision for
|
|
universal errors.
|
|
|
|
``defer`` may be followed by an arbitrary statement. The compiler
|
|
should reject a ``defer`` action that might terminate early, whether by
|
|
throwing or with ``return``, ``break``, or ``continue``.
|
|
|
|
Example::
|
|
|
|
if exists(filename) {
|
|
let file = open(filename, O_READ)
|
|
defer close(file)
|
|
|
|
while let line = try file.readline() {
|
|
...
|
|
}
|
|
|
|
// close occurs here, at the end of the formal scope.
|
|
}
|
|
|
|
If there are multiple defer statements in a scope, they are guaranteed
|
|
to be executed in reverse order of appearance. That is::
|
|
|
|
let file1 = open("hello.txt")
|
|
defer close(file1)
|
|
let file2 = open("world.txt")
|
|
defer close(file2)
|
|
...
|
|
// file2 will be closed first.
|
|
|
|
A potential extension is to provide a convenient way to mark that a
|
|
defer action should only be taken if an error is thrown. This is a
|
|
convenient shorthand for controlling the action with a flag. We will
|
|
evaluate whether adding complexity to handle this case is justified
|
|
based on real-world usage experience.
|
|
|
|
Importing Cocoa
|
|
---------------
|
|
|
|
If possible, Swift's error-handling model should transparently work
|
|
with the SDK with a minimal amount of effort from framework owners.
|
|
|
|
We believe that we can cover the vast majority of Objective-C APIs
|
|
with ``NSError**`` out-parameters by importing them as ``throws`` and
|
|
removing the error clause from their signature. That is, a method
|
|
like this one from ``NSAttributedString``::
|
|
|
|
- (NSData *)dataFromRange:(NSRange)range
|
|
documentAttributes:(NSDictionary *)dict
|
|
error:(NSError **)error;
|
|
|
|
would be imported as::
|
|
|
|
func dataFromRange(_ range: NSRange,
|
|
documentAttributes dict: NSDictionary) throws -> NSData
|
|
|
|
There are a number of cases to consider, but we expect that most can
|
|
be automatically imported without extra annotation in the SDK, by
|
|
using a couple of simple heuristics:
|
|
|
|
* The most common pattern is a ``BOOL`` result, where a false value
|
|
means an error occurred. This seems unambiguous.
|
|
|
|
* Also common is a pointer result, where a ``nil`` result usually
|
|
means an error occurred. This appears to be universal in
|
|
Objective-C; APIs that can return ``nil`` results seem to do so via
|
|
out-parameters. So it seems to be safe to make a policy decision
|
|
that it's okay to assume that a ``nil`` result is an error by
|
|
default.
|
|
|
|
If the pattern for a method is that a ``nil`` result means it produced
|
|
an error, then the result can be imported as a non-optional type.
|
|
|
|
* A few APIs return ``void``. As far as I can tell, for all of these,
|
|
the caller is expected to check for a non-``nil`` error.
|
|
|
|
For other sentinel cases, we can consider adding a new clang attribute
|
|
to indicate to the compiler what the sentinel is:
|
|
|
|
* There are several APIs returning ``NSInteger`` or ``NSUInteger``. At
|
|
least some of these return 0 on error, but that doesn't seem like a
|
|
reasonable general assumption.
|
|
|
|
* ``AVFoundation`` provides a couple methods returning
|
|
``AVKeyValueStatus``. These produce an error if the API returned
|
|
``AVKeyValueStatusFailed``, which, interestingly enough, is not the
|
|
zero value.
|
|
|
|
The clang attribute would specify how to test the return value for an
|
|
error. For example::
|
|
|
|
+ (NSInteger)writePropertyList:(id)plist
|
|
toStream:(NSOutputStream *)stream
|
|
format:(NSPropertyListFormat)format
|
|
options:(NSPropertyListWriteOptions)opt
|
|
error:(out NSError **)error
|
|
NS_ERROR_RESULT(0);
|
|
|
|
- (AVKeyValueStatus)statusOfValueForKey:(NSString *)key
|
|
error:(NSError **)
|
|
NS_ERROR_RESULT(AVKeyValueStatusFailed);
|
|
|
|
We should also provide a Clang attribute which specifies that the
|
|
correct way to test for an error is to check the out-parameter. Both
|
|
of these attributes could potentially be used by the static analyzer,
|
|
not just Swift. (For example, they could try to detect an invalid
|
|
error check.)
|
|
|
|
Cases that do not match the automatically imported patterns and that
|
|
lack an attribute would be left unmodified (i.e., they'd keep their
|
|
NSErrorPointer argument) and considered "not awesome" in the SDK
|
|
auditing tool. These will still be usable in Swift: callers will get
|
|
the NSError back like they do today, and have to throw the result
|
|
manually.
|
|
|
|
For initializers, importing an initializer as throwing takes
|
|
precedence over importing it as failable. That is, an imported
|
|
initializer with a nullable result and an error parameter would be
|
|
imported as throwing. Throwing initializers have very similar
|
|
constraints to failable initializers; in a way, it's just a new axis
|
|
of failability.
|
|
|
|
One limitation of this approach is that we need to be able to reconstruct
|
|
the selector to use when an overload of a method is introduced. For this
|
|
reason, the import is likely to be limited to methods where the error
|
|
parameter is the last one and the corresponding selector
|
|
chunk is either ``error:`` or the first chunk (see below). Empirically,
|
|
this seems to do the right thing for all but two sets of APIs in the
|
|
public API:
|
|
|
|
* The ``ISyncSessionDriverDelegate`` category on ``NSObject`` declares
|
|
half-a-dozen methods like this::
|
|
|
|
- (BOOL)sessionDriver:(ISyncSessionDriver *)sender
|
|
didRegisterClientAndReturnError:(NSError **)outError;
|
|
|
|
Fortunately, these delegate methods were all deprecated in Lion, and
|
|
are thus unavailable in Swift.
|
|
|
|
* ``NSFileCoordinator`` has half a dozen methods where the ``error:``
|
|
clause is second-to-last, followed by a block argument. These
|
|
methods are not deprecated as far as I know.
|
|
|
|
The above translation rule would import methods like this one from
|
|
``NSDocument``::
|
|
|
|
- (NSDocument *)duplicateAndReturnError:(NSError **)outError;
|
|
|
|
like so::
|
|
|
|
func duplicateAndReturnError() throws -> NSDocument
|
|
|
|
The ``AndReturnError`` bit is common but far from universal; consider
|
|
this method from ``NSManagedObject``::
|
|
|
|
- (BOOL)validateForDelete:(NSError **)error;
|
|
|
|
This would be imported as::
|
|
|
|
func validateForDelete() throws
|
|
|
|
This is a really nice import, and it's somewhat unfortunate that we
|
|
can't import ``duplicateAndReturnError:`` as ``duplicate()``.
|
|
|
|
|
|
Potential future extensions to this model
|
|
-----------------------------------------
|
|
|
|
We believe that the proposal above is sufficient to provide a huge
|
|
step forward in error handling in Swift programs, but there is always
|
|
more to consider in the future. Some specific things we've discussed
|
|
(and may come back to in the future) but don't consider to be core to
|
|
the Swift 2.0 model are:
|
|
|
|
Higher-order polymorphism
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
We should make it easy to write higher-order functions that behave
|
|
polymorphically with respect to whether their arguments throw. This
|
|
can be done in a fairly simple way: a function can declare that it
|
|
throws if any of a set of named arguments do. As an example (using
|
|
strawman syntax)::
|
|
|
|
func map<T, U>(_ array: [T], fn: T -> U) throwsIf(fn) -> [U] {
|
|
...
|
|
}
|
|
|
|
There's no need for a more complex logical operator than disjunction
|
|
for normal higher-order stuff.
|
|
|
|
This feature is highly desired (e.g. it would allow many otherwise
|
|
redundant overloads to be collapsed into a single definition), but it
|
|
may or may not make it into Swift 2.0 based on schedule limitations.
|
|
|
|
Generic polymorphism
|
|
~~~~~~~~~~~~~~~~~~~~
|
|
|
|
For similar reasons to higher-order polymorphism, we should consider
|
|
making it easier to parameterize protocols on whether their operations
|
|
can throw. This would allow the writing of generic algorithms, e.g.
|
|
over ``Sequence``, that handle both conformances that cannot throw (like
|
|
``Array``) and those that can (like a hypothetical cloud-backed
|
|
implementation).
|
|
|
|
However, this would be a very complex feature, yet to be designed, and
|
|
it is far out-of-scope for Swift 2.0. In the meantime, most standard
|
|
protocols will be written to not allow throwing conformances, so as to
|
|
not burden the use of common generic algorithms with spurious
|
|
error-handling code.
|
|
|
|
Statement-like functions
|
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Some functions are designed to take trailing closures that feel like
|
|
sub-statements. For example, ``autoreleasepool`` can be used this way::
|
|
|
|
autoreleasepool {
|
|
foo()
|
|
}
|
|
|
|
The error-handling model doesn't cause major problems for this. The
|
|
compiler can infer that the closure throws, and ``autoreleasepool``
|
|
can be overloaded on whether its argument closure throws; the
|
|
overload that takes a throwing closures would itself throw.
|
|
|
|
There is one minor usability problem here, though. If the closure
|
|
contains throwing expressions, those expressions must be explicitly
|
|
marked within the closure with ``try``. However, from the compiler's
|
|
perspective, the call to ``autoreleasepool`` is also a call that
|
|
can throw, and so it must also be marked with ``try``::
|
|
|
|
try autoreleasepool { // 'try' is required here...
|
|
let string = try parseString() // ...and here.
|
|
...
|
|
}
|
|
|
|
This marking feels redundant. We want functions like
|
|
``autoreleasepool`` to feel like statements, but marks inside built-in
|
|
statements like ``if`` don't require the outer statement to be marked.
|
|
It would be better if the compiler didn't require the outer ``try``.
|
|
|
|
On the other hand, the "statement-like" story already has a number of
|
|
other holes: for example, ``break``, ``continue``, and ``return``
|
|
behave differently in the argument closure than in statements. In the
|
|
future, we may consider fixing that; that fix will also need to
|
|
address the error-propagation problem.
|
|
|
|
``using``
|
|
~~~~~~~~~
|
|
|
|
A ``using`` statement would acquire a resource, holds it for a fixed
|
|
period of time, optionally binds it to a name, and then releases it
|
|
whenever the controlled statement exits. ``using`` has many
|
|
similarities to ``defer``. It does not subsume ``defer``, which is useful
|
|
for many ad-hoc and tokenless clean-ups. But it could be convenient
|
|
for the common pattern of a type-directed clean-up.
|
|
|
|
Automatically importing CoreFoundation and C functions
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
CF APIs use ``CFErrorRef`` pretty reliably, but there are several
|
|
problems here: 1) the memory management rules for CFErrors are unclear
|
|
and potentially inconsistent. 2) we need to know when an error is
|
|
raised.
|
|
|
|
In principle, we could import POSIX functions into Swift as throwing
|
|
functions, filling in the error from ``errno``. It's nearly impossible
|
|
to imagine doing this with an automatic import rule, however; much
|
|
more likely, we'd need to wrap them all in an overlay.
|
|
|
|
In both cases, it is possible to pull these into the Swift error
|
|
handling model, but because this is likely to require massive SDK
|
|
annotations it is considered out of scope for iOS 9/OS X 10.11 & Swift 2.0.
|
|
|
|
Unexpected and universal errors
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
As discussed above, we believe that we can extend our current model to
|
|
support untyped propagation for universal errors. Doing this well,
|
|
and in particular doing it without completely sacrificing code size
|
|
and performance, will take a significant amount of planning and
|
|
insight. For this reason, it is considered well out of scope for
|
|
Swift 2.0.
|
|
|