Run a throwable function without try, is this expected?

In following code example:

class Foo {
    private let queue = DispatchQueue(label: "foo.queue")
    enum Error: Swift.Error {
        case unknown
    }

    func throwError() throws {
        throw Foo.Error.unknown
    }
}

Normally, if you call a throwable function along with a closure which throws as well in a rethrow function, like the following extension, a complier error will prompt saying

a function declared 'rethrows' may only throw if its parameter does

extension Foo {
    func withErrorThrown<ExecutionResult>(execute: () throws -> ExecutionResult) rethrows -> ExecutionResult {
        try throwError() // Error: Call can throw, but the error is not handled; a function declared 'rethrows' may only throw if its parameter does
        return try execute()
    }
}

However, if you wrap them in a sync function from an instance of a DispatchQueue, the prompt goes away and compiles fine:

extension Foo {
    func withErrorThrown<ExecutionResult>(execute: () throws -> ExecutionResult) rethrows -> ExecutionResult {
        return try self.queue.sync {
            try throwError()
            return try execute()
        }
    }
}

And you can call it without try (because it only rethrows) and the error skips handling and causes crash:

let string = Foo().withErrorThrown(execute: { "" })
/// Compiles fine

But the code will crash with error when runs:

error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).
The process has been left at the point where it was interrupted, use "thread return -x" to return to the state before expression evaluation.

I am aware that this is wrong implementation with the rethrows, the correct one should be to change rethrows to throws, because a throwable function always gets called in this case.

But I think this kind of implementation should at least generate a warning like the first extension.

There's nothing special about DispathQueue.async here. The following has the same behavior.

extension Foo {
    func withErrorThrown<ExecutionResult>(execute: () throws -> ExecutionResult) rethrows -> ExecutionResult {
        func sync<T>(handler: () throws -> (T)) rethrows -> T {
            try handler()
        }

        return try sync {
            try throwError()
            return try execute()
        }
    }
}

Yes. Actually any rethrow function that takes a throwable closure can reproduce this issue. I am just using an existing function to demonstrate the problem. Apology if the description is misleading.

This is SR‐680.

3 Likes