47

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?

  • 11
    You may want to add Go to the list, if we just ignore panic which is not quite the same. In addition what is said there, an exception is not much more than a sophisticated (but comfortable) way to perform a GOTO, although nobody calls it that way, for obvious reasons. – JensG Oct 3 '14 at 18:51
  • 3
    The sort answer to your question is that you need language support for exceptions in order to write them. Language support generally includes memory management; since an exception can be thrown anywhere and caught anywhere, there needs to be a way to dispose of objects that doesn't rely on the control flow. – Robert Harvey Oct 3 '14 at 19:07
  • 1
    I'm not quite following you, @Robert. C++ manages to support exceptions without garbage collection. – Karl Bielefeldt Oct 3 '14 at 19:14
  • 3
    @KarlBielefeldt: At great expense, from what I understand. Then again, is there anything that's undertaken in C++ without great expense, at least in effort and required domain knowledge? – Robert Harvey Oct 3 '14 at 19:15
  • 2
    @RobertHarvey: Good point. I'm one of those folks who doesn't think hard enough about these things. I've been lulled into thinking that ARC is GC, but, of course, it's not. So basically (if I'm grasping, roughly), exceptions would be a messy thing (C++ notwithstanding) in a language where disposing of objects relied on control flow? – orome Oct 3 '14 at 19:16
33

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."

  • 1
    So Future is essentially a way of examining the value returned from a function, without stopping to wait for it. Like Swift, it's return-value based, but unlike Swift, the response to the return value can occur at a later time (a bit like exceptions). Right? – orome Oct 4 '14 at 20:52
  • 1
    You understand a Future correctly, but I think you might be mischaracterizing Swift. See the first part of this stackoverflow answer, for example. – Karl Bielefeldt Oct 4 '14 at 20:57
  • Hmm, I'm new to Swift, so that answer is a bit hard for me to parse. But if I'm not mistaken: that essentially passes a handler that can be invoked at a later time; right? – orome Oct 4 '14 at 21:09
  • Yes. You're basically creating a callback when an error occurs. – Karl Bielefeldt Oct 4 '14 at 21:14
  • Either would be a better example IMHO – Paweł Prażak Feb 25 '16 at 17:49
19

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.)

  • Is it correct that kind of type system that Swift has, including optionals, is the sort of "powerful type system" that achieves this? – orome Oct 4 '14 at 12:45
  • 2
    Yes, optionals and, more generally, sum types (referred to as "enum" in Swift/Rust) can achieve this. It takes some extra work to make them pleasant to use, however: in Swift, this is achieved with the optional chaining syntax; in Haskell, this is achieved with monadic do-notation. – Rufflewind Oct 4 '14 at 17:32
  • Can a "sufficiently powerful type system" give a stack trace, if not it's pretty useless – Paweł Prażak Feb 25 '16 at 17:41
  • 1
    +1 for pointing out that exceptions obscure the flow of control. Just as an auxilary note: It stands to reason whether exceptions are not actually more evil than goto: The goto is restricted to a single function, which is a pretty small range provided function really are small, the exception acts more like some cross of goto and come from (see en.wikipedia.org/wiki/INTERCAL ;-) ). It can pretty much connect any two pieces of code, possibly skipping over code some third functions. The only thing it can't do, which goto can, is going back. – cmaster Aug 28 '16 at 15:10
  • 2
    @PawełPrażak When working with lots of higher-order functions, stack traces aren't nearly as valuable. Strong guarantees about inputs and outputs and avoidance of side effects are what prevent this indirection from causing confusing bugs. – Jack Aug 29 '16 at 18:11
10

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.

  • 1
    the catch's purpose is to make an object consistent (or terminate something if it's not possible) after an error occurred. – JoulinRouge May 31 '16 at 8:13
  • @JoulinRouge I know. But some languages have decided not to give you that opportunity, but instead to crash the entire process. Those language designers know the kind of cleanup you'd like to do but have concluded it is too tricky to give you that, and the trade-offs involved in doing so would not be worth it. I realize you may not agree with their choice ... but it's valuable to know they made the choice consciously for these particular reasons. – Charlie Flowers Sep 23 '16 at 15:01
6

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());
}
  • 1
    So is it fair to say that this too (like Go's approach) is similar to Swift, which has assert, but no catch? – orome Oct 4 '14 at 12:43
  • 1
    In Swift, try! means: Yes I know this could fail, but I'm sure it won't, so I'm not handling it, and if it fails then my code is wrong, so please crash in that case. – gnasher729 Aug 29 '16 at 16:46
6

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 :)

3

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.

  • For Swift, this is the correct answer, IMO. Swift has to stay compatible with existing Objective-C system frameworks, so under the hood they don't have traditional exceptions. I wrote a blog post a while ago on how ObjC works for error handling: orangejuiceliberationfront.com/… – uliwitness Dec 19 '16 at 17:02
2
 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.

  • And, likewise, those calls to ...throw_ioerror() return errors rather than throwing exceptions? – orome Oct 3 '14 at 19:37
  • 1
    @raxacoricofallapatorius If we are claiming exceptions don't exist then I assume the program follows the usual pattern of returning error codes on failure and 0 on success. – stonemetal Oct 3 '14 at 19:56
  • 1
    @stonemetal Some languages, like Rust and Haskell, use the type system to return something more meaningful than an error code, without adding hidden exit points the way exceptions do. A Rust function, may, for example, return a Result<T, E> enum, which can be either an Ok<T>, or an Err<E>, with T being the type you wanted if any, and E being a type representing an error. Pattern matching and some particular methods simplify handling of both successes and failures. In short, don't assume lack of exceptions automatically means error codes. – 8bittree May 31 '16 at 15:51
1

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.

  • Well, in reality, these exceptions can often be handled in just one place. For example, in a "download data update" function if the SSL fails or the DNS is unresolvable or the web server returns a 404, it doesn't matter, catch it at the top and report the error to the user. – Zan Lynx Aug 29 '16 at 22:56

protected by gnat Aug 28 '16 at 15:08

Thank you for your interest in this question. Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).

Would you like to answer one of these unanswered questions instead?

Not the answer you're looking for? Browse other questions tagged or ask your own question.