Kiwicode.dev blog
· Kinga Wilczek

SE-0518

~Sendable Conformance Syntax

Make intentional non-Sendable APIs visible at the declaration site.

Mental model: the tilde crosses out the default Sendable promise.

explicit intent library-facing compile-time only

Why it exists

1. Silence was ambiguous
2. Old workaround blocked subclasses
3. Proposal adds a true opt-out marker
Problem

Silence was ambiguous

Leaving off Sendable did not tell readers whether a type was forgotten, intentionally non-Sendable, or only incidentally safe today because its current members happen to be Sendable.

For public APIs, authors may want to say "do not pass this across concurrency domains" early, so clients do not build around a guarantee the library never meant to make.

Workaround

Old opt-out hurt subclasses

Making Sendable unavailable could express the intent, but for classes it also blocked safe subclasses from adopting Sendable later.

The whole hierarchy could get trapped by the base declaration.

New syntax

~Sendable makes the opt-out explicit

Write : ~Sendable on a nominal type declaration to say the type is intentionally non-Sendable.

It lives right beside the rest of the type's conformance list.

Payoff

Clearer APIs, cleaner migration

The language gets a real source-level opt-out that works with explicit-sendable checking and lets each subclass stay honest about its own guarantees.

Safe subclasses may opt in later; unsafe ones can remain non-Sendable.

THE CORE IDEA

The proposal turns a missing conformance into an explicit, reviewable decision.

Without a marker
// Before
public struct JobResult {
    var value: NonSendable
}

// Did the author forget Sendable?
// Or reject it on purpose?
// The declaration cannot tell you.
With ~Sendable
// After
public struct JobResult: ~Sendable {
    var value: NonSendable
}

// Intent is documented in the type itself.
// Works beside other conformances too.
struct Tagged: Equatable, ~Sendable { }
ambiguity removed

CLASS HIERARCHY MEMORY TRICK

Old workaround shuts the whole tree. ~Sendable only marks the base as intentionally non-Sendable.

@available(*, unavailable)

Blocks Sendable for every subclass too.

Base class declares the workaround
Safe subclass Still blocked

Cannot add Sendable even if its design is sound.

Unsafe subclass Blocked too

The whole branch inherits the same closed door.

Even a thread-safe subclass cannot opt back in.

Base: ~Sendable

The base is explicit, but subclasses still choose.

Base class intent is stated directly
Safe subclass Can opt in
@unchecked Sendable
Unsafe subclass Stays non-Sendable

No pressure to promise what it cannot guarantee.

The base stays explicit while each subclass tells the truth about itself.

WHERE IT FITS

Only on nominal type declarations. It is not a general-purpose generic or protocol marker.

Allowed
struct Cache: ~Sendable { }
enum Token: ~Sendable { }
class Base: ~Sendable { }
struct Box: Equatable, ~Sendable { }
extension Container: Sendable
    where T: Sendable { }
// regular conditional Sendable conformance still works
Not allowed
extension Cache: ~Sendable { }
protocol P: ~Sendable { }
struct Holder<T: ~Sendable> { }
actor Worker: ~Sendable { }
struct S: ~Sendable, Sendable { }
// actors are already Sendable; no double signal allowed

FAST FACTS

What to remember when you see the syntax in a review or codebase.

Marker protocol -> compile-time only

Sendable has no runtime representation, so is / as? Sendable casts are impossible.

Great with explicit-sendable audits

This audit pushes API authors to spell out the Sendable story instead of relying on silence or inference. In the checked 6.4-dev nightly, the minimal declaration-side warning switch was -Wwarning ExplicitSendable; add -enable-experimental-feature TildeSendable only if you want to write ~Sendable, and treat -require-explicit-sendable as a recognized older switch that was not needed to surface the warning in this setup.

Conditional Sendable still works

~Sendable does not block conditional conformance. A type can stay explicitly non-Sendable by default and still add Sendable later when generic constraints such as T: Sendable make that promise valid.

Cannot fight inherited Sendable

You cannot write ~Sendable on something that is already Sendable through another rule, such as actors, global-actor-isolated types, Sendable protocols, or Sendable superclasses.

Mostly library-facing

Public APIs benefit most because readers need to know your intent.

Part of the tilde family

Useful mental model: like ~Copyable, ~Escapable, and ~BitwiseCopyable, the tilde spells an explicit opt-out. The semantics differ, but the source-level pattern is similar.

TRY IT TODAY

Use a nightly main-development snapshot toolchain. One switch lets you actually write ~Sendable; another surfaces the explicit-sendable diagnostics for silent public types.

Xcode
// Other Swift Flags (separate entries)
// try ~Sendable
-enable-experimental-feature
TildeSendable

// show explicit-sendable diagnostics
-Wwarning
ExplicitSendable

// also recognized in the checked nightly
// -require-explicit-sendable
SwiftPM
// Package.swift -> target swiftSettings
.target(
  name: "MyModule",
  swiftSettings: [
    .unsafeFlags([
      // try ~Sendable
      "-enable-experimental-feature", "TildeSendable",

      // show explicit-sendable diagnostics
      "-Wwarning", "ExplicitSendable",

      // also recognized in the checked nightly
      // "-require-explicit-sendable",
    ])
  ]
)

TO BE SENDABLE, OR NOT TO BE SENDABLE?

Interpretive question, not a rule: should an app try to make almost everything Sendable by default?

Why teams like it

Pro: safer by default

Concurrency mistakes surface early instead of turning into accidental shared mutable state.

Pro: easier APIs to trust

A Sendable-first codebase makes it simpler to move values across tasks and actors later.

Pro: pushes clean boundaries

It nudges stateful or thread-bound parts into smaller, more explicit isolation zones.

Why teams resist it

Con: over-constraining design

Not every type is meant to cross concurrency domains, especially UI and local mutable models.

Con: false comfort

Developers may reach for wrappers, locks, or @unchecked just to satisfy the checker.

Con: cognitive overhead

A strict Sendable-everywhere mindset can make simple app code feel more ceremonial than useful.

Balanced takeaway

~Sendable exists because making intent explicit is often better than pretending every type should participate in cross-concurrency transfer. Strong defaults help, but forcing them everywhere can blur design truth.

REFERENCES