Search code examples
swiftcancellationswift-concurrency

Cancellation not propagated


Cancellation is not propagated through the service class here

let task = Task<Service.Resource, Error> {
    if Task.isCancelled {
        throw URLError(.cancelled)
    }
    return try await service.loadResource()
}

What modification is required on this function to propagate cancellation?

func loadResource() async throws -> Resource {
    let data = try await load()
    return try decoder.decode(Resource.self, from: data)
}

Solution

  • Within structured concurrency, if you cancel a Task, its children will be canceled, too. If you are not seeing the children canceled, one of the following is a likely culprit:

    • perhaps the load method has introduced additional unstructured concurrency (i.e., another Task object was introduced inside load method);
    • perhaps the load method is not using a cancelable async method;
    • perhaps the attempt to call cancel of the task variable was done incorrectly … we would need to see the lifecycle of this task variable and where you called cancel method on its Task object.

    But, bottom line, you enjoy cancelation propagation if (a) you remain with structured concurrency inside your Task; (b) you await a method that supports cancelation.


    As an aside, consider the following:

    let task = Task<Service.Resource, Error> {
        if Task.isCancelled {
            throw URLError(.cancelled)
        }
        return try await service.loadResource()
    }
    

    That can be simplified to:

    let task = Task<Service.Resource, Error> {
        try Task.checkCancellation()
        return try await service.loadResource()
    }
    

    Unfortunately, this will be of limited utility. If the the task is canceled after it has started (and because of actor-reentrancy, this is likely), you are probably past this test, awaiting the loadResource (which is likely awaiting load). What is more important is that loadResource (and the load method that it calls) must both support cancelation.

    People will commonly encourage others to check if Task.isCancelled {…} or call try Task.checkCancellation(). But more importantly, make sure that load supports cancelation. E.g., if you are calling the async methods of URLSession, such as data(for:delegate:) or data(from:delegate:), they support cancelation already.

    But if you are using some other other API that predates Swift concurrency (perhaps something wrapped in a withCheckedContinuation or withUnsafeContinuation), then you would often wrap that in a withTaskCancellationHandler, taking advantage of whatever cancelation support that underlying API provides.

    But again, it is hard to advise further without seeing the implementation of the load method (and how you are canceling this top-level task).