[Pitch] Proposal for `safe` keyword in closures to simplify `weak` capture pattern

Hello.

I’d like to propose a potential enhancement to Swift that could simplify a common pattern used in closures — the weak self (or weak variable) capture followed by a guard let check.

Currently, we often see code like this:

class DataManager {
    private let service = NetworkService()
    private var data: [String] = []

    func fetchData() {
        service.fetch { [weak self] result in
            guard let self else {
                return
            }

            data = result
        }
    }
}

This pattern works well to avoid retain cycles, but it can be somewhat verbose and repetitive. After seeing this pattern in many projects, I thought it might be beneficial to make it more concise and intuitive. My suggestion is to introduce a new capture type called safe, which could apply not only to self, but also to other variables that can be captured as weak or unowned:

class DataManager {
    private let service = NetworkService()
    private var data: [String] = []

    func fetchData() {
        service.fetch { [safe self] result in
            data = result
        }
    }
}

The idea behind this is twofold:

  1. Simplified syntax: No need to write guard let self (or other variables) repeatedly. The safe keyword would handle this automatically.

  2. Compiler/runtime optimization: The safe keyword could allow the compiler or runtime to decide whether to capture self or other variables as weak or unowned, optimizing for both safety and performance based on the closure’s context.

This could improve readability and productivity, not just for cases involving self, but also for other variables that benefit from being captured weakly or as unowned.

You can view the proposal draft on GitHub Gist at NNNN-safe-keyword-for-closures

I’d love to hear your thoughts!

Best regards,
-Van

2 Likes

I have two main issues with this. The first is a terminology one: Swift has chosen to use safe to mean a specific kind of safety, and that kind of safety is not involved here.

The second is a more serious one: I'm opposed to people using this pattern without carefully thinking about it, because it can be unsafe. It's only safe if "silently do nothing" is actually what you wanted, and in my experience that's very often not true. This problem is so widespread that the compiler team had to alter the language's lifetime semantics because people were incorrectly relying on weak references to be non-nil in situations where the original design of the language said they could be nil. We also see many examples of people reflexively "protecting" themselves from retain cycles where no retain cycle can possibly occur.

27 Likes

Thank you for your thoughtful feedback! I completely understand your concerns, and I’d like to address both of them.

First, regarding the terminology: I agree that the word safe may not be the best fit considering Swift’s established use of the term. We can change the keyword to something more appropriate, and I’d love to hear suggestions from the community. The main goal here is to simplify the syntax, so I think we can be flexible on what the actual keyword should be.

As for the broader issue of safety, I recognize that careful consideration is always needed when managing memory, particularly with weak references. However, I’d argue that, in many cases, developers have already stopped carefully thinking through this pattern, as we often see a reflexive use of guard let self else { return }. By introducing a more concise syntax, the proposal aims to reduce boilerplate, but I fully agree that it should never encourage ignoring safety concerns.

Perhaps an option could be to provide this feature in contexts where it's most beneficial and ensure that we properly document best practices to avoid misuse. The goal is not to bypass proper memory management but to simplify the most common, well-understood cases where this pattern is already being applied.

Thank you again for raising these points. I’m looking forward to hearing more thoughts on how we can improve this idea!

Best regards,
-Van

2 Likes

I proposed this back in 2021: `guard` capture specifier for closure capture lists

The core team shared this feedback at the time:

Since then, SE-0345 (if let self instead of if let self = self) and SE-0365 (support for implicit self after self has been unwrapped) both improved the boilerplate situation enough that I don't really think any other sugar is necessary. guard let self else { return } is pretty reasonably lightweight :slight_smile:

12 Likes

Maybe {[guarded self] in} for any closure that doesn’t return.

I have hundreds of closures in one of my large projects, almost all of them return nothing, and have guard else’s that just return. Although it is unlikely they will ever be called in any circumstances where self or any other captured variables are nil, as the weak/unowned capture is often only there to break a reference cycle.

To suffice the condition where an guard-else needs to return we could have a nice new statement for that

{[guarded self] in
    guardfailed {
        return “Failed!”
    }

    // some awesome stuff here
    return “Worked!”


}

something like “guardfailed” could be mandatory.

1 Like
{ [guarded self] in 
  guardfailed { return "Failed!" }
  return self.string
 }

// vs

{ [weak self] in 
  guard let self else { return "Failed!" }
  return self.string
}

The difference in characters here is minimal for (IMO) unneccessary syntax sugar.

I think there's potentially room for a better way to handle avoiding retain cycles, but I don't think syntax sugar is it. I think it's going to need a more radical rethink of things.

9 Likes

I think most closures don’t return anything so getting to eliding “guardfailed” would eliminate that entire line. Additionally capture lists that guard more than self or one variable, would be reduced to just one “guardfailed”.

I actually don’t support this idea anymore than I support trailing closures (they solved nothing it was all to look pretty). But after thinking it over, I almost have myself convinced on this one.

This is actually an area that annoys me. Writing if let self else {return} hundreds of time is silly.

But it’s not a “problem”, necessarily. I am ok with status-quo.

I think it is worth the discussion. I want to see where this goes.

2 Likes

Your example of fetching data asynchronously is best served with an actual strong capture. For any closure that is executed immediately and then released after execution, a strong capture is almost always preferable. This applies to completion handlers, animation blocks, thread hopping, background tasks, and more.

An exception is event handlers and other use cases where the closure is retained and/or called multiple times. For these closures, you'd normally want to explicitly spell out how to deal with released self.

Implicit no-op closures are a surefire way to create subtle bugs.

That is not to say that I'm not sympathetic to the fact that retain cycles can also give rise to subtle bugs. But what I'm saying is that any syntactic sugar to help avoid cycles, needs to also make the programmer think about what to do in when self is released.

11 Likes

Thank you for your response! I think there might be a misunderstanding about the use case I’m presenting.

In the example of fetching data asynchronously, a strong capture would indeed prevent self from being released, even if the object (such as a UIViewController) has already been dismissed or is no longer needed. This could easily lead to a retain cycle, keeping self in memory longer than necessary, which is precisely the problem we want to avoid in these situations.

The goal of the [safe self] proposal is to simplify the process of using weak captures, not to replace strong captures where they are appropriate. In scenarios where we don’t want to retain self unnecessarily, [safe self] ensures that if self is still around, it can be used safely without having to explicitly check for nil with a guard let self. This pattern is already quite common (guard let self else { return }), and the proposal simply makes it more concise and less prone to human error.

For event handlers or closures retained over time, where careful thought is required, [safe] wouldn’t alter the need for explicit management of self and memory. The intent is to streamline the syntax in cases where weak captures are the standard, without compromising the developer’s control over memory management.

I appreciate your feedback and would be happy to discuss further if there are any other points you’d like to explore.

Best regards,
-Van

Thank you for your feedback! You raise a valid point about closures that return values. While such closures are less common in practice (in the context of this proposal), here are some initial complementary ideas for [safe self] to handle them effectively without sacrificing simplicity

Handling Return Values in [safe self]:

For closures that return values, here are a few ways to manage self being released:

  1. Optional Return Types: If the closure returns an optional value, [safe self] can default to nil when self is released:
let closure: () -> Int? = { [safe self] in
    return self.performSomeCalculation()
}
  1. Custom Fallbacks: For non-optional return types, we could allow developers to specify a fallback value:
let closure: () -> Int = { [safe self or 0] in
    return self.performSomeCalculation()
}
  1. Throwing Closures: In throwing closures, [safe self] could throw a custom error if self is released:
let closure: () throws -> Int = { [safe self or throw MyError.selfReleased] in
    return try self.performSomeCalculation()
}

Anyway, in most cases, where closures are non-throwing and don’t return values, [safe self] will be ideal for reducing boilerplate without sacrificing safety.

The ideal scenario for [safe self] continues to be in cases like this:

let closure = { [safe self] in
    self.performSomeAction()
}

It’s important to note that the current pattern with guard let or if let would not be replaced by this proposal. Developers could continue using them when more explicit handling of self is necessary. The goal here is to make code easier to write and, more importantly, easier to read.

Thanks again for your input!

Best regards,
-Van

I wonder if ?? could be used instead of introducing the or keyword.

2 Likes

Yes, indeed. Thank you!

In your example DataManager holds a NetworkService strongly. When you call fetchData(), this may cause the network service to hold a strong reference to the closure, which again may hold a strong reference to DataManager, causing a potential cycle:

DataManager → NetworkService → closure → DataManager

However, this cycle is only temporary, as the NetworkService should release the closure after calling it. By introducing a weak capture in the closure, won't really change much, unless you have expensive calculations in your completion handler.

Usually, the default strong capture is preferable for almost every kind of closure. The guard let self else { return } pattern is indeed common, but it is often an anti-pattern.

If, on the other hand, your network service had some kind of operation queue which would enqueue the fetch request, and that it may or may not be called in the foreseeable future—then certainly the cycle should preferably be broken as soon as the NetworkService is no longer needed. But for most fire-wait-complete operations, the weak dance is simply not needed.

The introduction and prevalence of Combine has certainly muddied the waters, but there's a reason Swift chose strong captures as the default. it is almost always correct.

For the cases where a weak capture with no-op semantics is correct, I would prefer the "no-op" to be blatantly obvious. That it is impossible to glance over the code and miss the fact that there are implicit dead paths there.

Maybe a [noop self] could work?

2 Likes

I would like to add a few more observations in the case of Task where self is captured as long as the Task executes, so for instance:

class Test {
    private var task: Task<Void, Never>?

    deinit {
        print("deinit called")
        task?.cancel()
    }

    func doSomething1()   {
        self.task = Task {
            print("Task started")
            do {
                try await self.asyncOperation()
                print("Task finished")
            } catch {
                print(error)
            }
        }
    }

    func doSomething2()   {
        self.task = Task { [weak self] in
            print("Task started")
            do {
                try await self?.asyncOperation()
                print("Task finished")
            } catch {
                print(error)
            }
        }
    }

    func doSomething3()   {
        self.task = Task {
            print("Task started")
            do {
                try await Task.sleep(for: .seconds(2))
                print("Task finished")
            } catch {
                print(error)
            }
        }
    }

    private func asyncOperation() async throws {
        try await Task.sleep(for: .seconds(2))
    }
}
var test: Test? = Test()
test?.doSomething1() // 2, 3
print("nil-out")
test = nil
print("is nil == \(test == nil)")

will print in cases:

  • doSomething1
[11:53:34,245]: Task started
[11:53:34,260]: nil-out
[11:53:34,260]: is nil == true
// ~2 seconds later
[11:53:36,373]: Task finished
[11:53:36,374]: deinit called
  • doSomething2 - same as doSomething1
[11:53:52,417]: Task started
[11:53:52,420]: nil-out
[11:53:52,420]: is nil == true
// ~2 seconds later
[11:53:54,481]: deinit called
[11:53:54,482]: Task finished
  • doSomething3 - immadietly cancelled on deinit because Task.sleep is used directly without using self.asyncOperation
[11:57:30,235]: Task started
[11:57:30,238]: nil-out
[11:57:30,238]: deinit called
[11:57:30,238]: dupa
[11:57:30,238]: is nil == true
[11:57:30,246]: CancellationError()

It does not matter if there is or not the assignment self.task = Task { ... } in terms of doSomething1 and doSomething2.

But, also with for loops/while/etc, it gets more subtle as you need to remember that you must be more careful, if you use weak and the guard let self else { return } at the proper place, the Task might keep strong reference as long as the loop is running.
e.g.:

    func doSomething4()   {
        self.task = Task { [weak self] in
            // By using the guard here, there is no difference from not using `[weak self]` at all, 
            // as soon the task is created the guard will keep a strong reference to `self`
            // guard let self else { return } 
            print("Task started")
            while true {
                guard !Task.isCancelled else { return }
                do {
                    try await self?.asyncOperation()
                } catch {
                    print(error)
                }
            }
        }
    }

With all that, [safe self] might make sense, if could behave like [unkowned self] but safely in terms of returning as soon as self is not available anymore.

However, by introducing

func stop() {
   task?.cancel()
}

and calling it before nil-out` makes more sense.

This is related to the helpers that people started to create like

extension Task {
    func store(in set: inout Set<AnyCancellable>) {
        set.insert(AnyCancellable(cancel))
    }
}
...
private var cancellableBag = Set<AnyCancellable>()
Task {
    ...
}
.store(in: &cancellableBag)

but as soon the Task uses self, this helper has the same downside as trying to cancel the ongoing task in deinit.

There is nothing safe about this (or conversely, nothing unsafe about the default semantics).

If you explicitly want to break early or do nothing when self is no longer around, the keyword should reflect that, because the "safe" thing to do might not be returning early at all. It might be performing cleanup, participating in collaborative cancellation, or many other things.

And, as you point out, if the capturing closure is async, then weak self might vanish at many different times during the execution of the closure. If we only guard against it at the beginning of the closure, then we might be in a situation where self is gone, but things are still computing. How "safe" is that?!

If we need an alternative syntax to [weak self] that does nothing and returns nothing, and only does so early, I think the spelling should reflect what it actually does — safe does not communicate anything! (I have suggested [noop self] but I'm not sure if its much better)

I fear people will just write [safe self] everywhere and not deal with the nuances of what's actually going on. And to future readers of the code (including yourself), the intent isn't clear!

One could argue that people already write [weak self] and guard let self else { return } without much thought, but at least that takes a little ceremony, and the return point is explicitly called out.

2 Likes

I have no opinion on the naming convention. I just see a potential to a new keyword to a weak handle to an object inside a task/closure with quick return/noop when no longer exists.

My contribution to topics such as this often seem to precipitate the end of the conversation, :frowning_face: but here it is anyway...

As a possibility for general syntax to improve on the weak/guard pattern, I've often wished for something like:

{ [guarded self, guarded a, b] in
    if guarded
    {
        // Do normal stuff.
        // self and a are non-optional.
    }
    else
    {
        // Return or do abnormal stuff.
        // Either self and/or a is nil.
    }
}

which could also be used as:

{ [guarded self, guarded a, b] in
    if !guarded
    {
        // Return or do abnormal stuff.
    }
    // Normal stuff, or continuing if there is no return in the above condition.
    // self and a are optionals because we have not passed through an "if guarded" check.
}

Or even:

{ [guarded self, guarded a, b] in
    guarded else
    {
        // Must return. Either self and/or a is nil.
    }
    // Do normal stuff. self and a are non-optional.
}

and these allow for any kind of return or throw, not just a Void, and is useful for more than just [weak self].

However, I think that the bigger issue is exactly why [weak self] is being used in the first place. In many cases, perhaps most cases, it is to avoid a retain cycle. Is it not the concept of retain cycles that need to be looked at, and whether a more formal syntax can be used to express their occurrence and avoidance?

At the very least, instead of saying [weak self], could we not say something like [nocycle self] to convey the purpose of the weakness to the compiler and possibly add to the ability to analyse the behaviour of the program?

2 Likes

Agreed!

In my own experience of closures that were correct to weakly capture self specifically, I believe I recall fewer cases where the closure needed to spell out anything special to do than instead just needing to exit ASAP. I'm sorry that examples don't come to mind, and I understand that this may not be everyone's experience.

For cases where handling is indeed needed when self is nil, I'd propose that the existing syntax is good as-is. Any new syntax adding bonus terseness in select cases doesn't have to be forcibly extended to fit all others too.

In those situations where they would, some solace can be taken that closures with strong references are the simplest ones to write.

Code with weak captures that needs explicit handling have a good syntax now with justifiable verbosity. It's unfortunate that code where the programmer is right to want a no-op closure currently has to live with that verbosity as well. While that's been a noticeable fraction of cases in code I've written, even if it were a small minority of cases it wouldn't make it less unfortunate for that code to be awkward and inelegant.

Hmm, yes I too fear any new sugar, be it [guard self] (whatever the spelling for this is decided to be) or something unrelated, where beginners might gravitate to and use like a silver bullet without thinking. I would indeed not want the meme among beginners to be "just add [guard self] to all closures to make cycles go away".

But does the additional verbosity of the current syntax really help much to make it less of a ill-considered silver bullet?

Apologies for coming to this thread a month late. Thanks @cal for the similar pitch back in 2021 and chiming in to remind us able the core teams feedback back then:

The core team talked about this, and we're reluctant to add features that introduce new sources of control flow unless they significantly improve expressive power.

And thanks as well for referencing the more recent accepted proposals:

Since then, SE-0345 (if let self instead of if let self = self) and SE-0365 (support for implicit self after self has been unwrapped) both improved the boilerplate situation enough that I don't really think any other sugar is necessary.

But those in fact lead me to instead reach the opposite conclusion! They provide a benchmark for the level of "improved expressive power" they're willing to accept, and in that light I find their rejection in 2021 to be wrongly decided.

May I ask @cal or anyone methodically diving back into the old thread, was there as was much bikeshedding consensus as I remember around the syntax [guard self]? I might be remembering poorly just because I thought that choice was preferable.

If I may float what I also thought made sense to me at the time, and still does: seeking to extend the terse new syntax to express default values for closures with results neither Optional or Void, those debates just detract from the pitch. Let new syntax work where it's meaning is well defined, and in other cases let the existing syntax be used. Or at least leave any extension for a follow-up pitch.

Do they? They don't seem to introduce any new sources of control flow to me.

2 Likes