Handling C++ exceptions

Sloppy code is sloppy, so I don't expect that every caller would be diligently calling the API with try -- the compiler is not enforcing it, and if the error is not common, software could even reasonably work.

Given the amount of code I've seen in questions using unnecessary optionals, IUOs, and direct porting from other languages like Java, if you add such a construct to the language it will be used and abused. Especially if it lets you skip try usage, as that can be an annoyance to users from languages that don't require explicit marking of failing operations.

4 Likes

+1. This would get abused if it was available. In normal Swift code, people should have to write try! at the usage site if this is the behavior they want.

Usually, though, the immediate caller of that function will not be well placed to do that cleanup. The typical cleanups that I think you'd want to do are releasing system-wide resources that the OS doesn't clean up automatically when the process terminates. That's typically not something the immediate caller of such a function will know how do to -- it's handled better by some global "termination handler" a la C++'s std::terminate_handler.

Maybe -- but with the current proposal, this sloppiness will at least be confined to the C++ interop boundary.

I'm still not convinced this will be needed in practice, and it will be easy to add once we have evidence that it is actually needed.

While I do want the ability to throw, propagate, and catch C++/ObjC exceptions to be a Swift feature eventually, I think it will always need to be opt-in as a build setting (beyond e.g. some simple mechanism for immediately catching things thrown from a C function). It would impose substantial overheads in both code size and runtime performance, and it's really not something we ever want to encourage. I definitely don't think I would favor giving it such an accessible spelling as throws!.

Just MHO, but I think that adding throws! as a general language feature is just fine. This is required to use it in PythonKit for dynamic callable and other things like that.

Furthermore, exactly the same "people will use it everywhere and it will cause tons of terrible code" argument was made by people for @DynamicMemberLookup, ImplicitlyUnwrappedOptional, operator overloading, emoji identifers (everyone will name everything :poop:!) and other features. In practice, if someone pervasively adopted and misused it, they would be writing bad code. There are lots of ways to write bad code in swift, we do not need to "prevent" them all.

-Chris

5 Likes

The difference here is that those constructs enabled new abuses. Thankfully most people haven’t gone out of their way to learn new capabilities just then also use them poorly. However, IUOs have definitely become a staple of new programmers and programmers from languages with no explicit nullability. They are pervasive. throws!, even more than IUOs, will be seen as a way to escape Swift’s “annoying” requirement to mark every throwing context, so I’d expect it to become prevalent in every code base produced by new to Swift engineers.

5 Likes

Additionally, the addition of throws! would need justification within Swift’s error story by itself, not merely as a construct brought along for C++. How would it fit within the reasoning expressed by the error manifesto, for example? I’m not sure it can.

2 Likes

Hi Jon,

I respectfully disagree with you. Everything you are saying about throws! applies to IUO's as well. I understand that you may not like IUO's, but they are critical for importing foreign APIs. It is also very important (from what I've seen in practice) that the language can express IUOs to be able to build wrapper logic in Swift itself.

I don't see any way that throws! is different from IUOs here. Yes, there is an opportunity for abuse, but that is true of literally everything in any language. This is handled through coding standards etc. If some library was built that pervasively used them in the wrong way, it would not get widely adopted - this is natural selection for libraries.

-Chris

9 Likes

I understand the role IUOs plays in Swift and use them regularly myself. There are differing opinions within the community, but I think they solve a critical pain point in the language. My point was that they're an easy lever to reach for when dealing with optionals and that, without proper guidance, new programmers and programmers from other languages will overuse them, to the detriment of their codebases and Swift code in the wild. The same thing is guaranteed to happen to throws!. It's an especially tempting target for developers from languages like Java which don't require explicit try statements on every line that can throw. However, it's vastly different behavior will cause greater issue in the long run.

So while I think it makes sense as a construct for imported API, it needs specific and careful consideration for additional as a general language feature intended to be used by Swift developers who aren't importing from other languages. What's the use case for it in Swift without interop with other languages? How can that use case be rectified with the logic laid out in the error manifesto, more than just "well, C++ import needed it"?

However, this seems like a discussion for when and if the feature is actually proposed.

1 Like

Those are good questions. Perhaps people will want to have this instead of automatically trapping on integer overflows and array out of bound accesses, and perhaps it'll end up abused. It's not limited to C++ though: interop with many languages out there would benefit. This should indeed be further discussed in a thread where throws! is the actual topic so everyone can participate.


But going back to the C++ discussion, I'm a bit concerned by the possibility that you might inadvertently make the compiler generate a C++ exception handler where it's probably not needed. Take this C++ method call enclosed in a bigger try expression where the Swift function might throw:

try swiftFunc(cppFunc())

How common is this going to be? Is this something worth worrying about? I'm not too sure. Or should we require a separate try directly before the C++ function to actually catch its exceptions?

try swiftFunc(try cppFunc())

That looks funny to me.

The type checker phase ordering is not well set up for overloading on throws currently, but if that were fixable, then it seems like overloading could be used to get the behavior of throws! in library-driven interop modules like PythonKit, having one call operator that throws available in try contexts, and another that traps available everywhere else. Maybe we'd still want throws! to be a modifier the Clang importer can synthesize for C++ interop, so that it doesn't have to generate two declarations for every imported C++ declaration, but it strikes me as not terribly useful for straight Swift code.

Question: If we did import C++ code with throws! then how would we deal with exposing the Error itself when the user attempts to handle it? IIRC C++ does not have a required root exception type - so I could an int just as easily as I throw a std::exception

Ideally, to imitate Swift's throwing by value, we'd have to pack the C++ exception by value into Swift.Any or std::any, however it does not seem like there's a standard way to copy the current exception without mentioning its type (which is infeasible, we would have to mention every single type). There might be a way to do that in certain C++ runtimes, by using the implementation details of the C++ runtime, we'd have to investigate it. The C++ standard offers us std::exception_ptr which I'm afraid we would have to use.

Passing them around is one thing, but Swift errors must also conform to Error. I suppose we could add the following declaration somewhere in a module overlay:

extension std.exception: Error { ... }

And this would allow std::exception exceptions to be propagated through Swift code. Types that do not conform to Error could just trap. You can fix that adding a conformance to Error to your library's root exception type(s).

Hopefully you'll never have to write extension Int: Error to use a C++ library. :roll_eyes:

Alternatively, we could create a class, call it CxxException, that conforms to Error and contains the C++ exception. Less elegant, but maybe more manageable?

Far more likely an extension on Int32 :wink:

I suspect between the limitations of C++ runtimes and need to map into Swift concepts, it would be viable to trap anything other than std::exception.

Yes, this seems reasonable to me.

To reiterate other people's points and respond to Jon's comments upthread - I agree about the potential for abuse and agree that former Java programmers who haven't learned enough Swift may reach for throws! when they shouldn't - just like they reach for IUOs when they shouldn't because they don't understand nullability. I am not personally afraid of that after seeing many people express concerns about similar language features - such abuses would surely happen in the small, but would not be considered "good swift code" and infect the ecosystem through successful libraries.

As to "why would we add this other than interoperability", others have pointed out that we already have the throws! behavior for core operators like + and a[i] on arrays, we just don't allow anyone to catch and handle the error. The semantic of throws! is perfectly safe: if you don't locally try the error, it locally traps, just like many core operations in Swift implicitly do.

I think that adding throws! and adopting them for these operators would make existing patterns more safe, and improve recoverability for those sorts of errors.

-Chris

8 Likes

I don't think anyone is arguing that this would happen. But many of us have had the misfortune of having worked in real world code bases where abuses do happen. This can be an expensive and difficult problem to solve.

Obviously this is going to happen in some organizations no matter what language is used. But when it's possible to omit a footgun without harming the rest of the language then I think it's a good idea.

If + and a[i] were spelled throws! and actually threw an error in their implementation would the compiler be able to optimize away the cost when users don't write try? If so, then I'm beginning to see the argument for throws!. I can imagine it being useful in cases where we would otherwise just trap.

2 Likes

I agree, but it's even more than this. As someone who does a lot of StackOverflow and GitHub issue support for a popular library, I see a lot of people's code. This is especially true of Swift beginners, whether they're new to programming or just new to Swift. It's extremely common for such users to use ! as a quick fix based on compiler fixits or code found online. Due to the prevalence of optionals in such codebases, the issue is quickly multiplied beyond what experienced Swift programmers see. Now, usage of throwing functions is far less than optionals in such code bases, if only due to such programmers not knowing that they should be using it, but the friction experienced is very similar. Luckily throws! would only impact user-created code, so i's overall impact would be much less, but I think still significant.

Crashing due to runtime assertions is far different than language features that let you avoid error checking. There are no errors produced in bounds checking, and Swift has long held the opinion that no useful error could be produced as the result of such an operation anyway, so crashing is the preferred result. So I don't think these are the same thing at all.

However, even if they were, throws! would still need to be justified in terms of the error manifesto, where explicit marking was a key design principle. Adding it would essentially make Swift adopt a hybrid model and lose a key benefit of explicit marking: being able to see, at a glance, which lines of code could produce an error, in any context, and requiring they be handled in some way.


All of that said, I like the previous suggestion of a bridged Error type. I think it would integrate well with the existing language and is the smaller change to the language.

1 Like