Subscribe to Jacob’s Tech Tavern for free to join 100,000 senior Swift engineers learning advanced concurrency, agentic iOS engineering, and Swift performance for 10 minutes a week.
It’s not every day a meme gives you imposter syndrome, but this did.
While I recognise many of these words, I frankly didn’t know what most of these actually do. I don’t even know what these things are called.
It turns out the term is “type attributes”.
The best way to understand type attributes is by checking how they’re implemented in the Swift source code.
Ha, just kidding, I’ll explain them here.
Type attributes provide the compiler with additional information about a type. When used in a function, they enforce constraints on the type they are applied to — in many cases, this type is a closure: () -> Void.
Overcoming Imposter Syndrome
Today, we’re going to work together to understand type attributes in detail by recreating the meme step-by-step — you’re welcome to open a Swift playground and code along.
Beginning, naturally, with hello world.
func allTheAttributes(_ then: () -> Void) {
then()
}
allTheAttributes {
print("Hello, world!")
}The then closure executes immediately, and so there’s no problem passing it in directly as a simple unadorned closure.
But what if the closure wasn’t immediately executed?
@escaping
This attribute indicates that a type in a method declaration can be stored for later execution. Therefore, a closure marked @escaping can outlive the lifetime of the function itself — it “escapes” the function scope, stored to be executed later.
Due to outliving the call site, escaping closures are a common source of memory leaks, which is why you’ll often be capturing [weak self] (or unowned self) to avoid this.
@escaping is only required for closures in function parameter position, i.e. closures directly used as arguments. If you wrapped this closure with an enum’s associated value (this happens every time you use an optional!), then it becomes implicitly escaping. This is why optional closures like (() -> Void)? don’t need to be @escaping.
Let’s update our function so that the closure can execute after the method finishes, by placing the () -> Void inside some other closure.
func allTheAttributes(_ then: () -> Void) {
Task {
then()
}
}Now, we get a compiler warning:
Escaping closure captures non-escaping parameter 'then'To address this, we can add the @escaping attribute to the closure type () -> Void and get our code compiling again!
func allTheAttributes(_ then: @escaping () -> Void) {
Task {
then()
}
}
allTheAttributes {
print("Hello, world!")
}@Sendable
Sendable might be the most wilfully misunderstood parts of Swift Concurrency.
We get it, you’re a compiler warning, shut up, I’ll fix you in September.
When a type is marked @Sendable, this means it’s thread-safe. Sendable types can be passed across arbitrary concurrency contexts without risking data races.
When a closure type is marked @Sendable, this means any values passed into the closure must also be Sendable, and can’t be captured by reference. Since a Sendable closure captures its parameters by value, it eliminates the possibility of corrupting data with conflicting data mutations on parallel threads.
If we turn on strict concurrency checking, we get a warning on our then() closure:
Capture of 'then' with non-sendable type '() -> Void' in a `@Sendable` closureThis is because the initialiser on Task itself takes a @Sendable closure:
public init(priority: TaskPriority? = nil, operation: @escaping @Sendable () async -> Success)Now we’re equipped with the tools to handle this error: we can simply tell the compiler that our own closure is also @Sendable, making it possible to pass into Task.
func allTheAttributes(_ then: @escaping @Sendable () -> Void) {
Task {
then()
}
}
allTheAttributes {
print("Hello, world!")
}And the compiler warning is thusly satisfied (until Swift 7 at least).



