Question

Many modern languages provide rich exception handling features, but Apple's Swift programming language does not provide an exception handling mechanism.

Steeped in exceptions as I am, I'm having trouble wrapping my mind around what this means. Swift has assertions, and of course return values; but I'm having trouble picturing how my exception-based way of thinking maps to a world without exceptions (and, for that matter, why such a world is desirable). Are there things I can't do in a language like Swift that I could do with exceptions? Do I gain something by losing exceptions?

How for example might I best express something like

try:
    operation_that_can_throw_ioerror()
except IOError:
    handle_the_exception_somehow()
else:
     # we don't want to catch the IOError if it's raised
    another_operation_that_can_throw_ioerror()
finally:
    something_we_always_need_to_do()

in a language (Swift, for example) that lacks exception handling?

Was it helpful?

Solution

In embedded programming, exceptions were traditionally not allowed, because the overhead of the stack unwinding you have to do was deemed an unacceptable variability when trying to maintain real-time performance. While smartphones could technically be considered real time platforms, they are powerful enough now where the old limitations of embedded systems don't really apply anymore. I just bring it up for the sake of thoroughness.

Exceptions are often supported in functional programming languages, but so rarely used that they may as well not be. One reason is lazy evaluation, which is done occasionally even in languages that are not lazy by default. Having a function that executes with a different stack than the place it was queued to execute makes it difficult to determine where to put your exception handler.

The other reason is first class functions allow for constructs like options and futures that give you the syntactic benefits of exceptions with more flexibility. In other words, the rest of the language is expressive enough that exceptions don't buy you anything.

I'm not familiar with Swift, but the little I've read about its error handling suggests they intended for error handling to follow more functional-style patterns. I've seen code examples with success and failure blocks that look very much like futures.

Here's an example using a Future from this Scala tutorial:

val f: Future[List[String]] = future {
  session.getRecentPosts
}
f onFailure {
  case t => println("An error has occured: " + t.getMessage)
}
f onSuccess {
  case posts => for (post <- posts) println(post)
}

You can see it has roughly the same structure as your example using exceptions. The future block is like a try. The onFailure block is like an exception handler. In Scala, as in most functional languages, Future is implemented completely using the language itself. It doesn't require any special syntax like exceptions do. That means you can define your own similar constructs. Maybe add a timeout block, for example, or logging functionality.

Additionally, you can pass the future around, return it from the function, store it in a data structure, or whatever. It's a first-class value. You're not limited like exceptions which must be propagated straight up the stack.

Options solve the error handling problem in a slightly different way, which works better for some use cases. You're not stuck with just the one method.

Those are the sorts of things you "gain by losing exceptions."

OTHER TIPS

Exceptions can make code more difficult to reason. While they aren't quite as powerful as gotos, they can cause many of the same problems due to their non-local nature. For example, let's say you have a piece of imperative code like this:

cleanMug();
brewCoffee();
pourCoffee();
drinkCoffee();

You can't tell at a glance whether any of these procedures can throw an exception. You have to read the documentation of each of these procedures to figure that out. (Some languages make this slightly easier by augmenting the type signature with this information.) The above code will compile just fine regardless of whether any of the procedures throw, making it really easy to forget to handle an exception.

Additionally, even if the intent is to propagate the exception back to the caller, one often needs to add additional code to prevent things from being left in an inconsistent state (e.g. if your coffeemaker breaks, you still need to clean up the mess and return the mug!). Thus, in many cases code that uses exceptions would look just as complex as as code that didn't because of the extra cleanup required.

Exceptions can be emulated with a sufficiently powerful type system. Many of the languages that avoid exceptions use return values to get the same behavior. It's similar to how it's done in C, but modern type systems usually make it more elegant and also harder to forget to handle the error condition. They may also provide syntactic sugar to make things less cumbersome, sometimes almost as clean as it would be with exceptions.

In particular, by embedding error handling into the type system rather than implementing as a separate feature allows "exceptions" to be used for other things that aren't even related to errors. (It's well known that exception handling are actually a property of monads.)

There are some great answers here, but I think one important reason has not been emphasized enough: When exceptions occur, objects can be left in invalid states. If you can "catch" an exception, then your exception handler code will be able to access and work with those invalid objects. That is going to go horribly wrongly unless the code for those objects was written perfectly, which is very, very difficult to do.

For example, imagine implementing Vector. If someone instantiates your Vector with a set of objects, but an exception occurs during the initialization (perhaps, say, while copying your objects into the newly allocated memory), it is very hard to correctly code the Vector implementation in such a way that no memory is leaked. This short paper by Stroustroup covers the Vector example.

And that is merely the tip of the iceberg. What if, for example, you had copied over some of the elements, but not all of the elements? To implement a container like Vector correctly, you almost have to make every action you take reversible, so the whole operation is atomic (like a database transaction). This is complicated, and most applications get it wrong. And even when it is done correctly, it greatly complicates the process of implementing the container.

So some modern languages have decided it is not worth it. Rust, for example, does have exceptions, but they cannot be "caught," so there is no way for code to interact with objects in an invalid state.

In my opinion, exceptions are an essential tool for detecting code errors at run time. Both in tests and in production. Make their messages verbose enough so in combination with a stack trace you can figure out what happened from a log.

Exceptions are mostly a development tool and a way to get reasonable error reports from production in unexpected cases.

Apart from separation of concerns (happy path with only expected errors vs. falling through until reaching some generic handler for unexpected errors) being a good thing, making your code more readable and maintainable, it is in fact impossible to prepare your code for all possible unexpected cases, even by bloating it with error handling code to complete unreadability.

That's actually the meaning of "unexpected".

Btw., what is expected and what not is a decision that can only be made at the call site. That's why the checked exceptions in Java didn't work out - the decision is made at the time of developing an API, when it is not at all clear what is expected or unexpected.

Simple example: a hash map's API can have two methods:

Value get(Key)

and

Option<Value> getOption(key)

the first throwing an exception if not found, the latter giving you an optional value. In some cases, the latter makes more sense, but in others, your code sinmply must expect there to be a value for a given key, so if there isn't one, that's an error that this code can't fix because a basic assumption has failed. In this case it's actually the desired behavior to fall out of the code path and down to some generic handler in case the call fails.

Code should never try to deal with failed basic assumptions.

Except by checking them and throwing well readable exceptions, of course.

Throwing exceptions is not evil but catching them may be. Don't try to fix unexpected errors. Catch exceptions in a few places where you wish to continue some loop or operation, log them, maybe report an unknown error, and that's it.

Catch blocks all over the place are a very bad idea.

Design your APIs in a way that makes it easy to express your intention, i.e. declaring whether you expect a certain case, like key not found, or not. Users of your API can then choose the throwing call for really unexpected cases only.

I guess that the reason that people resent exceptions and go too far by omitting this crucial tool for automation of error handling and better separation of concerns from new languages are bad experiences.

That, and some misunderstanding about what they are actually good for.

Simulating them by doing EVERYTHING through monadic binding makes your code less readable and maintaintable, and you end up without a stack trace, which makes this approach WAY worse.

Functional style error handling is great for expected error cases.

Let exception handling automatically take care of all the rest, that's what it's for :)

One thing I was initially surprised about the Rust language is that it doesn't support catch exceptions. You can throw exceptions, but only the runtime can catch them when a task (think thread, but not always a separate OS thread) dies; if you start a task yourself, then you can ask whether it exited normally or whether it fail!()ed.

As such it is not idiomatic to fail very often. The few cases where it does happen are, for example, in the test harness (which doesn't know what the user code is like), as the top-level of a compiler (most compilers fork instead), or when invoking a callback on user input.

Instead, the common idiom is to use the Result template to explicitly pass up errors that should be handled. This is made significantly easier by the try! macro, which is can be wrapped around any expression that yields a Result and yields the successful arm if there is one, or otherwise returns early from the function.

use std::io::IoResult;
use std::io::File;

fn load_file(name: &Path) -> IoResult<String>
{
    let mut file = try!(File::open(name));
    let s = try!(file.read_to_string());
    return Ok(s);
}

fn main()
{
    print!("{}", load_file(&Path::new("/tmp/hello")).unwrap());
}

Swift uses the same principles here as Objective-C, just more consequently. In Objective-C, exceptions indicate programming errors. They are not handled except by crash reporting tools. "Exception handling" is done by fixing the code. (There are some ahem exceptions. For example in inter process communications. But that is quite rare and many people never run into it. And Objective-C actually has try / catch / finally / throw, but you rarely use them). Swift just removed the possibility to catch exceptions.

Swift has a feature that looks like exception handling but is just enforced error handling. Historically, Objective-C had a quite pervasive error handling pattern: A method would either return a BOOL (YES for success) or an object reference (nil for failure, not nil for success), and have a parameter "pointer to NSError*" which would be used to store an NSError reference. Swift automagically converts calls to such a method into something looking like exception handling.

In general, Swift functions can easily return alternatives, like a result if a function worked fine and an error if it failed; that makes error handling a lot easier. But the answer to the original question: The Swift designers obviously felt that creating a safe language, and writing successful code in such a language, is easier if the language doesn't have exceptions.

 int result;
 if((result = operation_that_can_throw_ioerror()) == IOError)
 {
  handle_the_exception_somehow();
 }
 else
 {
   # we don't want to catch the IOError if it's raised
   result = another_operation_that_can_throw_ioerror();
 }
 result |= something_we_always_need_to_do();
 return result;

In C you would end up with something like the above.

Are there things I can't do in Swift that I could do with exceptions?

No, there isn't anything. You just end up handling result codes instead of exceptions.
Exceptions allow you to reorganize your code so that error handling is separate from your happy path code, but that is about it.

In addition to the Charlie's answer:

These examples of declared exception handling that you see in many manuals and books, look very smart only on very small examples.

Even if you put aside the argument about invalid object state, they always bring about a really huge pain when dealing with a large app.

For example, when you have to deal with IO, using some cryptography, you may have 20 kinds of exceptions that may be thrown out of 50 methods. Imagine the amount of exception handling code you will need. Exception handling will take several times more code than the code itself.

In reality you know when exception can't appear and you just never need to write so much exception handling, so you just use some workarounds to ignore declared exceptions. In my practice, only about 5% of declared exceptions need to be handled in code to have a reliable app.

Licensed under: CC-BY-SA with attribution
scroll top