Handling C++ exceptions

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

I've realized that I had been making an implicit assumption about throws! in user-written code that isn't true.

My wrong assumption was that if function a() was marked throws!, then in the body of a() we would be allowed to call other throwing functions without prefixing those calls with a try; errors from those throwing functions would implicitly propagate to the caller of a() as if we had written try in front of each call.

But of course none of this follows. The throws! marker on a() only specifies that the try is optional for callers of a(), not for the implementation of a(). And, of course, if the caller of a() doesn't add a try to the call, exceptions thrown in a() cause a trap; they don't propagate further up the call stack.

Having realized this, I think I'm much more comfortable with allowing throws! in user code. A function marked throws! still has to explicitly put a try on every throwing function it calls, so it's not making life easier for itself; it's merely making life easier for its callers (if those callers are OK with trapping on an error). And if a function wants to make life easier for its callers in this way, that's already possible today by putting a try! in front of every call to a throwing function. So I think I've realized there isn't really a lot of sloppiness that throws! allows that isn't possible today.

I'm against the whole concept of throw! because it make writing safe code harder.

Actually, when you call a function that throw but forget to take that into account, the compiler raise an error.

With throw!, you now have to carefully check the signature of all methods you call to make sure they do not throw!.

I understand this may be required for importing API, but I don't see how it can be useful elsewhere (unlike IUO that where useful before PropertyWrapper to define late init ivar).

Limiting its usage to imported API would guarantee that it cannot be present anywhere, and would prevent the need to carefully check any calls.

2 Likes

Today, any function that we call can in turn call fatalError -- how is throws! different?

1 Like

I think throws! is a good replacement for fatalError in some cases but it is a bad replacement for throws. The danger is that people use throws! too liberally as a substitute for the latter.

1 Like

Not A Contribution
A quick question to help me determine if I think throws! would be acceptable and usable, similar to IUOs.

What would happen in this scenario?

import PythonKit

// This function should become throws! right?
// (Since it handles Python inter and that operation would raise an exception)
func wrapper_throwing() /* throws! */ {
    PythonObject(42) / PythonObject("42")
}

// Does this function become throws! implicitly as well?
func other_func() {
    wrapper_throwing()
}

// If yes, then I should be able to do this, right?
do {
    try other_func()
} catch { print(error) }

Would forbidding throws! in public APIs help reduce the danger? We could make an exception for @inlinable functions. This way the optimizer will always have a chance at optimizing away the actual throwing.

throws! is never implicit. It simply causes the caller to insert an implicit try! before calling the function.

So assuming the / operator function is throws!, this:

PythonObject(42) / PythonObject("42")

would be equivalent to this code:

try! PythonObject(42) / PythonObject("42")

So no error can escape from wrapper_throwing, it'll just trap.

The difference is that you can propagate the error if you decide it's worth it. Just do it the normal way:

func wrapper_throwing() throws {
    try PythonObject(42) / PythonObject("42")
}
2 Likes

Okay, I think the way people are talking about throws! now is completely unrelated to the original subject of this thread.

1 Like

I don't think so. The overflow and array subscript examples are public API. The danger I'm talking about is most likely to happen in application code where public is irrelevant.

If the optimizer is able to handle this well in the cases where it is most useful it may be a tradeoff worth making. But the tradeoff will remain and there will be codebases where it is abused and causes problems.

Thanks for pointing out the thread creep. I agree that throws! deserves a separate discussion, as it's (potentially) not just about C++ interop.

Also, to be clear, I don't have any plans to tackle throws! in the near term. If and when I do, I will write up a Swift Evolution proposal.

The first step for exceptions would be to implement the "trap on uncaught C++ exception" logic discussed up-thread. Actually propagating C++ exceptions will come later and will require a lot more beyond throws! -- C++'s std::exception base class defines virtual functions, so support for polymorphism would need to be implemented first, among other things.

Based on the discussion here, I've submitted a PR that adds a section on exceptions to the C++ interop manifesto:

Please comment!

Edit: I see that @gribozavr has already triggered a merge for the PR, but please comment nonetheless. I'll address comments in a followup PR.

Right! I would be very opposed to implicit propagation of errors. That isn't implied by adding throws! to the language.

-Chris