Syntactically nicer way of handling errors in Swift

I have an idea that I "borrowed" from Ruby which I feel might be a nice addition to the Swift language when handling methods exceptions. Those who are familiar with rescue SomeError => e might know where this is going already.

Normally when we are dealing with exceptions, our code might look something like this:

func finishedFetchingData() {
    do {
        let models = try transform([Drink(), Drink()])
        self.state = .loaded(models)
    } catch(let error) {
        print("An error happened... \(error.localizedDescription)")
    }
}

Which is fine. However, I propose a syntactically nicer way of handling this and that is to have the ability to place the catch block at the end of the method. Which would look something like this:

func finishedFetchingData() {
    let models = try transform([Drink(), Drink()])
    self.state = .loaded(models)

rescue(let error):
    print("An error happened... \(error.localizedDescription)")
}

My primary reason for this is to eradicate some of the nesting that's forced on you due to the do block. You then have to define the bulk of your logic inside the do portion of the method due to variables that might have been defined in that scope.

Normally if you were to use the try keyword when not in the scope of a do block, the compiler would error with "Errors thrown from here are not handled" however, if you were to supply a rescue block at the end of the method it would compile fine because our errors are handled there.

Just to clarify, the rescue keyword is placed at the very end of the method, even after any return statement as this is a special case only in the result of an exception being thrown.

Ta,
John

-1 on this for multiple reasons.

  • I personally don't like the design, it hurts readability from my point of view.
  • This design does not scale, you could have multiple do blocks in a single function (even nested), while this design only has a single catch path or all at the very end of the function.
  • This is only sugar and does not enable anything new nor does it add any ergonomics already mentioned in my second point.
5 Likes

That's a fair point, I agree that with this current suggestion it wouldn't scale but surely there wouldn't be many cases where you'd nest do blocks inside each other because that also would sacrifice conciseness of the function, although I'm sure there are many examples out there of people doing this.

And yes, it is sugar that doesn't enable anything new, I never claimed that it did.

At this point it should at least enable more ergonomics, which from my point of view it doesn't. The bar for inclusion of syntax sugar proposals is very high and I don't think this pitch would justify it. However this is completely my personal opinion, others can happily disagree with what I said. ;)


Other than that, there are some threads where it was pitched by core team members that we could have switch like function bodies to enable more ergonomics for enums. If this could be somehow generalized we potentially could achieve a similar workflow like you pitched by transforming the value you operate on into Result first. However I don't remember which thread it was (something about enum ergonomics).

If I recall correctly C++ can do this (Ruby too), more this syntax though:

func finishedFetchingData() do {
    let models = try transform([Drink(), Drink()])
    self.state = .loaded(models)
} catch {
    print(error)
}

I'd like it, but I'm not sure it adds so much value it’s worth it. C++ and Ruby added everything anyone ever wanted and both are a mess because of it.

  • This design does not scale, you could have multiple do blocks in a single function (even nested), while this design only has a single catch path or all at the very end of the function.

This is spurious, you can still add multiple do blocks.

Ultimately, if it was there, I'd use it in every project I have written. I think it would be quite idiomatic if you couldn't throw in such a situation, thus your function is either annotated do or throws (see my example). This places a reasonable restriction on the do case for the sake of maintaining readability for function definitions.

I’ve been expecting this pitch since Swift was open sourced.

1 Like

Fair enough, I do appreciate your input (both of you)

A nicer more scalable approach would be to have multiple rescue blocks that could cast the error into the particular error but I'm not sure that would work out of the box.

I'm also surprised it took so long @Max_Howell1 :joy:

Still possible:

func finishedFetchingData() do {
    let models = try transform([Drink(), Drink()])
    self.state = .loaded(models)
} catch CocoaError {
    print("CocoaError:", error)
} catch {
    print(error)
}

As said I'd enjoy this addition and I think it'd be nice, but it may not be widely considered worth it.

1 Like

I'm not familiar with C++ but looking at this I do prefer this approach to using rescue! Be nice hearing other peoples opinions on this though.

Refer to this idea and comments bellow:

3 Likes

You can already very nearly write this anyway

func finishedFetchingData() { do {
    let models = try transform([Drink(), Drink()])
    self.state = .loaded(models)
} catch {
    print(error)
}}

which is approximately the same issue as we've had previously coming up with a syntax for methods where the entire body is a switch statement, as @DevAndArtist mentions.

2 Likes