How to handle errors in Swift when coming from a Java world

Hello,

I have difficulty with error handling in Swift. I understand how error handling works at the language level but I don't understand how I should manage errors in practice.

I have a Java background. And I see error handling as something to manage at higher level in my application. Like if I try to do a certain operation then, I have to deal with the error if it fails. With Swift, we have to deal with the error at a much lower level and I do not see which strategy I should use do so.

For example, I can define different protocols for components of my application, so I can easily choose a different implementation latter on. Let say I have:

protocol Something {
    func do()
}

class Worker: Something {
   func do() throws {
   // ...
   }
}

Let say Worker is a new implementation for Something and this new implementation is disk based so things can fail. But the protocol Something says that do() do not throw. This new implementation fails to implement my protocol properly.

How I should manage this? I am sorry if I am not very precise but I fell confused. Are other people having similar difficulties? I really would like to be pointed in the right direction.

You could mark it as throws in the protocol and then the implementation can decide whether it wants to throw or not.

It would be a bad idea if Swift offered the ability to define a non-throwing method in a protocol and allowed the implementation to throw because users of the protocol will not be expecting the implementation to throw.

You can create a refined protocol where the function is not marked as throws, but the base is marked as throws, if you want.

For example:

protocol Base {
  func foo() throws -> Int {}
}

protocol Refined: Base {
  func foo() -> Int
}

struct Foo: Refined {
  func foo() -> Int { return 0 }
}

let instance = Foo()
let _ = instance.foo() // Ok
let _ = (instance as Refined).foo() // Ok
let _ = (instance as Base).foo() // Error because protocol 'Base' has 'foo()' marked as 'throws', so it needs to be handled
let _ - try (instance as Base).foo() // Ok

With two protocols like above, it gives you a bit of flexibility, however you now have two protocols. The other way would be what I said initially, just have the function marked as throws and then the implementation is free to choose what it wants to do.

You should change the declaration of do() in the protocol to say the method throws.

This aspect of Swift errors is actually very similar to Java. In Java, if an interface doesn't declare that a method throws a given checked exception, classes that extend the interface can't throw it. The differences are that Swift doesn't make you specify the exact error types—just whether or not it throws at all—and that there's no equivalent to an unchecked exception.

But unchecked exceptions change everything. First there was checked exceptions is Java and then it was considered as an error. But in Swift it is like there is only checked exceptions.

I understand the semantic of all this. It is just that I don't know how to manage it.

For example, if there is an io error at a low level in my application then, how I can I make things flow up to high level to deal with the error? This is what I don't understand.

Each layer must then explicitly marked as throwable, but usually you don‘t need that much error handling if only your code base is not designed around errors extensively.

func foo() throws

func bar() throws {
  try foo()
}

do {
  try bar()
} catch {
  print(error)
}

So if foo emits an error, bar will fail and rethrow it which in the above example will be catched in the do/catch block. All error codes in a non-throwing scope are handled in do/catch (analog if you‘re curious enough, pitched async/await is started from a beginAsync function within a non-async scope).

Think about which operations should be performing I/O and make those operations throw. Make pure data processing operations not throw.

Keep in mind that I/O doesn’t only throw errors; it also blocks. Blocking the main thread is a big no-no in Apple frameworks—it causes unresponsive apps, stuttery animations, and other bad behavior. That means you need to know which operations perform I/O anyway so you can handle them asynchronously or in the background. Since you already need to handle those calls specially, you’ll be able to add error handling to those areas.

This seems to be a reasonable approach. I guess it is Swift idiomatic. Thank you.

The other option here is to decide that certain I/O errors are irrecoverable and crash your process if one occurs. This may make sense under specific circumstances. For example, I always use try! in code like the following because if my app’s process is so hosed that it can’t find the Document directory then it’s not going to be running for much longer anyway.

let docDir = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

I'm glad I am not the only one. I was feeling so guilty for initializing an instance var using

private let path = try! basePath().appendingPathComponent("locations.json")

IMHO, there is definitely something missing from Swift's current error handling. Basically, the use cases you have are:

  1. An operation that can and often enough does fail to return a result / perform some action (e.g. not every matrix can be inverted)
  2. An operation that usually succeeds but can fail in some rare cases, e.g. a timeout
  3. An operation that can only fail due to a programmer error; such as illegal array access, arithmetic overflow, division, using ! when you have proved to yourself the value can never be nil etc.

Solutions exist for 1. (an Optional or some kind of Result type, although throws can also be used) and 2. (usually throws, because it's less overhead to bubble up the stack), but no good story for 3. The "cleanest" solution would be to use something like fatalError (which has the advantage of just crashing instead of silently continuing with wrong values), but this has bad implications from a resiliency point of view: In many cases, especially on the server, you don't want to bring everything down, you want to be able to recover from somewhere (and if only to return a useful error message to the client or to clean up some resources).

So you're left with either using fatalError and having a suboptimal resilience story (that you need to tackle in some other way, e.g. by using load balancers and containers with health checks), or you need to awkwardly mix up your application logic with your resilience mechanisms by e.g. making almost everything throwable (or, even worse, you just silently continue calculating with wrong results). This is where you'd want to have some other mechanism; either unchecked exceptions as in many OOP languages, or some supervisor tree approach like in Erlang, or something else entirely.

But if you use try! it won't log the error so it will make it hard to debug. No?

try! is crashing the process? Not just the thread? I want to double check because I fell this is quite severe.

Right, ! crashes the entire app, which is why it should only be used in situations where further execution doesn't make sense, like the environment being broken.

1 Like

But if you use try! it won't log the error so it will make it hard
to debug. No?

Correct. If I deployed this app to a wide number of users and found that this line was failing regularly, I might change my approach (either to log the error or attempt a recovery). In this case, however, I’m pretty darned sure that an error from FileManager.url(for:in:appropriateFor:create:) is not my fault, and the most useful diagnostic here would be a sysdiagnose log for the relevant folks at Apple to look at.

try! is crashing the process?

Correct.

I want to double check because I fell this is quite severe.

That depends on your context. In a typical iOS app, crashing the process generally isn’t a big deal. The user relaunches your app and continues on with their work. If there’s a possibility that the user might lose a significant amount of work, you should already have a strategy in place for persisting that.

Keep in mind that, if you continue running when things are completely hosed — like, in my example, where FileManager.url(for:in:appropriateFor:create:) has failed — it’s unlikely that you’ll continue much further. Unless you’re fanatical about validating all of your error handling code paths, you’ll likely run into a bug in one of them. Additionally, it’s possible that the original failure was collateral damage from some underlying failure, and that will cause other failures elsewhere in your process. If you continue running you could crash somewhere else, perhaps in framework code, something that’s much harder to debug.

There’s a balance to be struck here:

  • How probable is the error?

  • What are the consequences of crashing?

  • How likely is it that you’ll be able to do any useful recovery?

  • How likely is it that you’ll make things worse by attempting a recovery?

  • How much does it cost you to write and test all that error handling code?

The answer here is different for each program and for the various errors within that program. My point is that crashing early is a valid option in some circumstances.


Finally, I want to stress that you shouldn’t get too hung up about the presence of a !. Many folks get worried by someArray.first! but wouldn’t bat an eyelid at someArray[0], even though the latter is more worrying IMO (because if someArray is actually an array slice, the first index won’t be 0).

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

10 Likes

This quite I think too. I fell something is missing about error handling in Swift. Are we alone thinking that?

Are we alone thinking that?

No, although most of the desire for this is coming from the server side. One advantage of Swift on the server is performance. To get that performance, you must use a single process to serve multiple requests. That’s problematic when the default response to these runtime checks is to terminate your process.

Fixing this is not easy, and it’s also not something that I’m involved in. If you’d like to help drive a solution, you should hop over to one of the areas where folks talk about The Future™:

  • Server if you’re specifically concerned with the server side of this

There’s been a bunch of threads about this before (especially in the server space), although I don’t have any specific references handy.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

2 Likes

I'll add to the picture the Swift Concurrency Manifesto by @Chris_Lattner3. Fault isolation is discussed in its "Part 3: Reliability through fault isolation" chapter.

I'm not quite sure this is the last version of this document. You can also search for more threads that talk about error tolerance with the "async/await" or "actor" keywords.

1 Like

We'd like to have fault tolerance / emergency recovery for the server use-case, but you shouldn't expect it to be compromise-free, and in the meantime I would encourage you to just accept faults on programmer errors rather than trying to radically reshape everything to be a recoverable error at the language level.

1 Like

Unfortunately, this isn't really an option for any kind of production app. One single bad edge case request that wasn't foreseen and the API is down for everybody?
Of course, it's possible to mitigate this by some other orchestration - load balancers and health checks, for example - but it's far from ideal.

I understand that server side Swift is still young and hasn't really been that much of a priority, but this is becoming a huge problem for us currently.

This would be one I opened. There was some useful discussion, but no outcome as of yet, as far as I can see.

Health checks and load balancers are good ideas anyway, and even if Swift perfectly isolated threads/actors and tore down faulty ones without compromise, it’s not obvious that doing that to an arbitrary thread/actor would leave you with a functioning system. Meanwhile, trying to make literally every precondition check in Swift into a recoverable error sure seems like it would leave you with an awful language.