Handling C++ exceptions

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