[weak self] + guard let self in a Task — A Pattern Worth Dropping

|

With completion handlers, [weak self] + guard let self was a clean pattern. The closure was stored externally, created a retain cycle, and the body was short and synchronous. Weak capture prevented the cycle; the guard was a cheap nil check at the top.

// Callbacks — this made sense
service.fetch { [weak self] result in
    guard let self else { return }
    self.update(result) // synchronous, done immediately
}

The pattern got carried into Swift concurrency by reflex, and it’s worth questioning it there.

func load() {
    Task { [weak self] in
        guard let self else { return }
        let data = await fetchData()
        let processed = await process(data)
        update(processed)
    }
}

guard let self at the top promotes the weak reference back to a strong one immediately. Self is now strongly retained for the entire task — across every suspension point, for however long the task runs. The weak capture only did work for the instant before the guard.

More importantly: Task { } doesn’t get stored by anyone externally the way a completion handler does, so there’s rarely a retain cycle to break in the first place.

What to do instead

If the task represents work that belongs to the object, just capture strongly:

func load() {
    Task {
        let data = await fetchData()
        update(data) // self kept alive for as long as the work runs — that's fine
    }
}

If you want to discard results when the object is gone by the time the async work finishes, check at the point where it matters — after the await:

func load() {
    Task { [weak self] in
        let data = await fetchData() // do the work regardless
        guard let self else { return } // discard if object is gone by now
        update(data)
    }
}
← Back to DevLog
rss facebook twitter github youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora