September 17, 2024·6 min read·engineeringswiftconcurrencyownershipreleases

Swift 6 makes data-race checking a language-mode contract

The opt-in Swift 6 mode turns isolation diagnostics into errors. Ownership, typed throws and shared libraries extend the release's emphasis on explicit program behavior.

A concurrency warning permits the build to continue with a potential data race. A language-mode error makes that same obligation part of what it means for the program to compile. Swift 6 introduces an opt-in mode that takes this second approach, while improving the analysis so more safe programs can express their intent without exemptions.

The September 17 announcement presents data-race safety alongside lower-level ownership support and broader platform libraries. The compiler release and the language mode are separate choices. Installing Swift 6 gives access to the new toolchain. Adopting Swift 6 mode makes the stronger isolation rules requirements for the code compiled in that mode.

I find the distinction useful because Swift's appeal includes explicit rules for mutation. Those rules need a practical adoption path when existing code and its dependencies predate the checking model. An opt-in language mode states the intended guarantee while allowing migration to proceed deliberately.

Better analysis makes enforcement practical

The Swift 5.10 announcement in March described complete data isolation checking through -strict-concurrency=complete. It also acknowledged conservative warnings for some patterns that further analysis could prove safe. Swift 6 makes progress on that second problem through better Sendable inference and analysis of transfers of mutable state between actors.

The underlying distinction is between sharing state and transferring it. A mutable value can cross an isolation boundary safely when the originating context cannot continue conflicting access. Rejecting every such transfer solely because the type lacks unrestricted sendability would impose unnecessary requirements on otherwise isolated code.

The announcement does not claim that all migration friction has disappeared. It calls usability an ongoing area of development. The immediate change is that code opting into Swift 6 mode receives compiler errors for potential data races, with fewer false positives than the previous complete-checking configuration.

This guarantee also has a scope. The March discussion explains explicit unsafe opt-outs such as nonisolated(unsafe) and @unchecked Sendable. Such declarations leave obligations with their authors. A compiler can enforce the checked model without proving arbitrary external synchronization that an unsafe annotation asks it to trust.

The new Synchronization library addresses lower-level needs with atomic operations and a mutex API. These are useful implementation tools for shared state. They do not remove the distinction between a type that safely permits shared access and a value whose access remains confined to one isolation region at a time.

Ownership now works through more generic code

Non-copyable types arrived in Swift 5.9 with ~Copyable. Swift 6 extends their participation in generics. This matters because a resource with unique ownership still needs ordinary abstractions around it. A restriction that works only in isolated concrete types would make reusable resource-handling code difficult to express.

The release demonstrates a protocol that does not require conforming types to be copyable. Both a copyable type and a non-copyable type can implement it. A generic function can consume either implementation. The protocol describes the operation needed by the function without imposing a copying capability that the operation does not require.

Standard library support follows the same direction. Optional and Result can wrap non-copyable values, and unsafe buffer pointer types can refer to non-copyable elements. The new Atomic type also uses non-copyability. These changes make ownership rules applicable through common containers and systems interfaces.

Enum pattern matching can now avoid copying payloads. This supports non-copyable payloads directly and can also reduce copying costs for copy-on-write values. A switch over an enum should not require a copy merely because the source uses a pattern to inspect the active case.

The same mechanism supports C++ move-only types through interoperability. Swift 6 can expose C++ types without copy constructors as non-copyable Swift types. The release also introduces an annotation for exposing a copyable C++ type as non-copyable in Swift when that is useful. That gives interoperability a way to preserve ownership constraints in the imported interface.

Error types become part of signatures

Typed throws lets a function name the error type it can produce. An unqualified throws remains equivalent to throws(any Error), while throws(Never) represents a non-throwing function. The release therefore relates throwing and non-throwing behavior through the same typed model.

A small parser can use the syntax without erasing the failure into an arbitrary error type. This example follows the release's typed-error model while keeping the computation self-contained.

enum CountError: Error {
    case negative
}

func checkedCount(_ value: Int) throws(CountError) -> Int {
    guard value >= 0 else {
        throw .negative
    }
    return value
}

The function either returns an integer or throws CountError. A caller's catch block can know that error type. Generic functions can propagate a closure's specific thrown error type, so an abstraction does not necessarily erase information that its argument already provides.

The announcement uses map to explain this. When the supplied closure throws a particular parsing error, the generic operation can preserve that type. When the closure is non-throwing, inference uses Never, making the resulting operation non-throwing too. The practical benefit concerns what the interface can state about error propagation.

Typed throws also matters to resource-constrained environments where allocating an erased error representation is unsuitable. Swift 6 includes an Embedded Swift preview for ARM and RISC-V bare-metal targets. The announcement still calls that mode experimental, so including it in the release does not imply stable support for every embedded use case.

Shared implementations reduce platform divergence

Swift 6 brings the portable Swift implementation of Foundation to Linux and Windows. The release identifies core types such as JSONDecoder, URL and Calendar among the reimplemented APIs. These share their implementation with macOS 15 and iOS 18, reducing the number of independently maintained implementations users encounter across platforms.

This is consequential for code whose portability depends on behavior, not merely the presence of a symbol with the same name. A common implementation provides a more direct basis for consistency. The announcement also makes APIs including FormatStyle and Predicate available across Swift platforms, alongside newly introduced Foundation capabilities.

Applications sensitive to binary size can import FoundationEssentials. That subset omits internationalization and localization data. The choice exposes a real dependency decision. A program that needs only the smaller set can avoid taking on data needed for facilities it does not use.

The release adds a fully static Linux SDK and supports cross-compilation with it. That enables executables that can run without installing external dependencies on the destination system. It also expands official testing to Debian and Fedora, plus Ubuntu 24.04. These changes address distribution and supported environments separately from source-language features.

Testing ships with the toolchain

Swift Testing arrives as an included library, so a test target can import Testing without declaring a separate package dependency. Its macro implementation is prebuilt. The announcement specifically points out that projects no longer need to build that library and its dependencies, including swift-syntax, just to use the included testing system.

That detail interests me because build times remain a source of friction in Swift. A macro-based test interface is more attractive when the toolchain supplies its implementation. It reduces the extra compilation work attached to adopting the testing library itself.

@Test declares test functions, while @Suite declares suites. #expect and #require capture expressions and their subvalues to produce detailed failures. Parameterized tests run an operation with multiple supplied arguments, keeping repeated cases attached to one test definition.

Swift Package Manager runs Swift Testing tests alongside existing XCTests and reports both. That coexistence matters for adoption because a project can use the included library without replacing every existing test immediately. The announcement says the library supports Apple's platforms, Linux and Windows.

Debugger work addresses another build cost

Explicit module builds let LLDB reuse locally built Swift and Clang modules from the project's artifacts. The release explains that this avoids recompiling implicit Clang module dependencies when the debugger first needs them. It can also reduce dependence on fragile header search-path reconstruction during that operation.

The new @DebugDescription macro addresses display of values in LLDB. It translates supported interpolations involving stored properties into type summaries, allowing the debugger's p command and variable displays to use custom formatting without executing arbitrary description code. This differs from calling a computed debugDescription through po.

These changes support the same practical goal as clearer concurrency and ownership interfaces, making the program's behavior easier to inspect. Swift 6 mode supplies stronger compile-time obligations, while the shipped libraries and debugger reduce some of the work surrounding those obligations. For a project adopting the release, language-mode selection remains an explicit decision alongside its platform and tooling dependencies.