Handling C++ exceptions in Swift: `SWIFT_THROWS` and `throws!`

Hi everyone,

I'd like to work on handling C++ exceptions from Swift and discuss how the APIs should be imported before getting too far into the implementation.

Currently, Swift cannot catch a C++ exception that escapes into Swift code. To handle one, we need a C++ wrapper that catches the exception and reports an error back to Swift. I'd like the compiler to generate that adaptation so callers can use ordinary try and do/catch. Current interoperability support

One approach would be an annotation in <swift/bridging>, something like SWIFT_THROWS:

#include <swift/bridging>

int checkedDivide(int numerator, int denominator) SWIFT_THROWS;

Swift would import this as:

func checkedDivide(_ numerator: CInt, _ denominator: CInt) throws -> CInt

And callers could handle a failure normally:

do {
  let result = try checkedDivide(42, 0)
  print(result)
} catch {
  print(error)
}

Unannotated APIs would keep their current behavior. Adding the annotation to an existing API would change its Swift interface, so library authors would need to account for callers now requiring try.

For the error representation, I'd start with a Swift type such as CxxException, containing a copied message: String. For exceptions derived from std::exception, that would come from what(). Other native C++ exceptions would use a fallback message.

This would let Swift callers handle the failure and read its message. Preserving the original C++ exception for type inspection or rethrowing would be a separate extension. I'd be interested in whether a message is enough for people's use cases, or whether losing the original exception would make this too limiting.

The implementation would catch the exception inside a generated C++ adapter, then propagate a Swift error. C++ would perform its normal cleanup inside that boundary, and Swift would use its existing throwing-call cleanup. Exceptions must not unwind through Swift frames.

I also found the earlier discussion about throws!, which seems worth revisiting here. The suggested behavior was:

// Hypothetical syntax:
func readValue() throws! -> Int

let a = readValue()      // Implicit try!: trap on failure
let b = try readValue()  // Propagate the error
let c = try? readValue() // Return nil on failure

That would allow existing calls to keep working while giving callers a way to opt into error handling, including for libraries whose headers they cannot annotate.

There are some language-design questions around that. For example, allowing a throws! method to satisfy a nonthrowing protocol requirement would need an explicit adaptation rule. A generated implementation could trap on failure, preserving the conformance, but calls through that protocol would still be nonthrowing. Adding try at the outer call site would not make the error recoverable. Function-value conversions would need rules too.

Another option I'd like to explore is an opt-in strict import mode, with a flag along these lines:

-cxx-exception-mode=strict

In that mode, supported C++ functions, methods, and constructors would import as ordinary throws unless Clang can establish that their exception specification is nonthrowing. noexcept functions would stay nonthrowing.

This would be a source-breaking option. Some automatic protocol conformances would no longer be possible because their requirements are nonthrowing. Potentially throwing operations that Swift cannot represent, such as throwing setters, would need to become unavailable.

Strict mode would also need to account for compiled Swift dependencies and the CxxStdlib overlay. That overlay contains nonthrowing conveniences backed by potentially throwing C++ operations. I'd initially keep strict mode experimental, require matching import policies in C++-enabled Swift dependencies, and omit incompatible Swift overlay conveniences while retaining access to raw C++ declarations.

These approaches could coexist: SWIFT_THROWS could require explicit handling for selected APIs, throws! could offer optional handling for unannotated imports, and strict mode could require handling across all supported explicit calls. I'd like to hear whether that combination makes sense or whether one approach should be the focus first.

I'll work on draft PRs to test the implementation and its behavior. My plan is to keep them small, starting with the exception bridge and free/static functions, then adding instance methods and value-type constructors separately. I'd initially test on Darwin and Linux behind an experimental feature flag, with coverage for cleanup and partially initialized objects.

Implicit copies and destructors would retain their existing behavior in the initial implementation. A violated noexcept contract would still terminate. Objective-C exceptions and runtime forced unwinding would remain outside the recovery supported by this feature.

For anyone involved in the previous discussions, has the direction changed since then? I'd particularly like feedback on whether SWIFT_THROWS is a reasonable first step, or whether it would be better to settle the throws! semantics before introducing an annotation.

3 Likes

I've opened draft PRs with a working prototype of the annotated imports and strict mode:

I'm keeping the feature PRs as drafts while we discuss the import behavior and error representation. Would it make sense to review the annotated bridge as a first experimental step, with strict mode following separately? I'd also appreciate feedback on whether the generated C++ adapter approach fits the direction of the existing interop implementation.

1 Like

Seems good enough for a prototype, but other than logging the what() message you can't do much with just this string. After catching you really should try to discriminate between an expected error you can handle and an unexpected one you have no code path ready for. This is usually done by checking the type and sometime the content of the exception.

Also, some frameworks/libraries out there define their own base exception type independent of std::exception (QException comes to mind). Likely having no information at all here is problematic too.

I'm not sure what's the best way to implement this, but perhaps CxxException could be a wrapper for std::exception_ptr. You could capture the exception with std::current_exception() and then try to "cast" it through a C++ helper function of this sort:

template<typename E>
E* exception_cast_as(std::exception_ptr eptr) {
    try {
        std::rethrow_exception(eptr);
        return nullptr; // unreachable
    } catch (const E& e) {
        return &e;
    } catch (...) {
        return nullptr;
    }
}

How to bridge this to be convenient on the Swift side I'm not sure.

How about, when an exception is thrown, checking if its type conforms to the Error protocol and throwing it directly. If it doesn’t, then you could make a CxxException wrapper struct that implements Error/LocalizedError as best as it can. Then people would be able to conform their specific exception base class (like std::exception or QException) to Error or LocalizedError to customize its Swift behavior

1 Like

Thanks for your replies!

QException actually inherits from std::exception. But you're right, some implementations might not do that. I wonder if that's something we should support since the interop is heavily dependent on STL anyways?

Either way, you're right - ideally we can preserve the type of the thrown error so we can branch in our catch blocks.

std::exception_ptr sounds like a good idea. I think I'll prototype this with AI. message can then reach into the std::exception's what() on access.

Good idea. I wonder if it's gonna be problematic that Error is Sendable?

The ideal API would probably be to support pattern matching for C++ exception types as well right?

catch let error as std.runtime_error {
    // ...
}

ooh, good question! Do you know if exceptions tend to contain shared mutable state? (I would hope not!)

That should work automatically as long as the type in question conforms to Error.

I prototyped exception bridging a while back and even pitched it. It… went poorly. But you might be able to salvage some of the code there if you want to dig it up.