Skip to content

Crash in withDeadline due to unsafe optional unwrap when both tasks complete with nil #4

@HassanTaleb90

Description

@HassanTaleb90

The custom withDeadline function crashes with a force unwrap on result! when both the timeout task and the body task complete with nil. This is possible if Task.sleep is cancelled and body() also throws or returns nil wrapped in Result?.self.

Problem line:

return result! // nil cannot occur here; it has been satisfied by either the sleep or the task

However, under certain conditions (like rapid cancellation), neither task may return a non-nil result, making the force unwrap unsafe and causing a crash.

✅ Suggested Fix:
Refactor the implementation to use withThrowingTaskGroup and avoid wrapping in Result?, eliminating the need for unsafe unwrapping and improving safety:

try await withThrowingTaskGroup(of: T.self) { group in
    group.addTask {
        try await Task.sleep(until: deadline, clock: clock)
        throw DeadlineFailure.timedOut(clock, deadline)
    }

    group.addTask {
        try await body()
    }

    let result = try await group.next()! // safe here, both tasks are added
    group.cancelAll()
    return result
}

This avoids crashing and ensures all edge cases are handled gracefully.

Code to test this crash:

static func getNTPTime() async -> Date? {
        let servers = ["time.apple.com", "pool.ntp.org", "time.cloudflare.com", "time.google.com"]

        return await withTaskGroup(of: Date?.self) { group in
            for server in servers {
                group.addTask {
                    do {
                        let ntpClient = NTPClient(
                            config: NTPClient.Config(version: .v4),
                            server: server
                        )

                        let response = try await ntpClient.query(timeout: .seconds(2))

                        let secondsComponent = Double(response.offset.components.seconds)
                        let attosecondsComponent = Double(response.offset.components.attoseconds) / 1_000_000_000_000_000_000
                        let offsetSeconds = secondsComponent + attosecondsComponent

                        let accurateDate = Date().addingTimeInterval(offsetSeconds)

                        let formatter = DateFormatter()
                        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
                        formatter.timeZone = .current

                        print("✅ Success from \(server)")
                        print("System time: \(formatter.string(from: Date()))")
                        print("NTP offset: \(offsetSeconds) seconds")
                        print("Accurate time: \(formatter.string(from: accurateDate))")

                        return accurateDate
                    } catch {
                        print("⚠️ Failed to get time from \(server): \(error)")
                        return nil
                    }
                }
            }

            for await result in group {
                if let date = result {
                    group.cancelAll() // Stop other pending tasks
                    return date
                }
            }

            return nil
        }
    }

Result:
Image

Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions