@sync block drops asynchronous errors in favor of synchronous errors

The @sync macro creates an array Any[] that is used to keep track of any @async tasks that are created within the (lexically-scoped) @sync block. After the code in the @sync block runs, the Base.sync_end function is called. This function waits on each task in the aforementioned array so as to make sure that control does not flow forward until after all of the @async tasks have completed.

You can check out the source code for the @sync and @async macros in the the task.jl source file.

The when the sync_end function iterates over this array of @async tasks to wait for each one of them, it catches any exceptions raised by the @async tasks and wraps them in a CompositeException, which is then thrown to @sync's caller. Here is an example of a CompositeException being thrown into outer scope after an asynchronous error within a @sync block:

julia> @sync begin
           t = @async begin
               error("asynchronous error")
           end
       end
ERROR: asynchronous error
error(::String) at ./error.jl:33
macro expansion at ./REPL[1]:3 [inlined]
(::getfield(Main, Symbol("##3#4")))() at ./task.jl:259
Stacktrace:
 [1] sync_end(::Array{Any,1}) at ./task.jl:226
 [2] top-level scope at task.jl:245

You can see in the stacktrace that sync_end was involved in catching the error raised by the async task. If multiple asynchronous errors arise, they are all included in the exception that is raised:

julia> @sync begin
           t1 = @async begin
               error("asynchronous error 1")
           end
           t2 = @async begin
               error("asynchronous error 2")
           end
       end
ERROR: asynchronous error 1
error(::String) at ./error.jl:33
macro expansion at ./REPL[3]:3 [inlined]
(::getfield(Main, Symbol("##5#7")))() at ./task.jl:259

...and 1 more exception(s).

Stacktrace:
 [1] sync_end(::Array{Any,1}) at ./task.jl:226
 [2] top-level scope at task.jl:245

Note the ... and 1 more exceptions(s) message, indicating that the second asynchronous error has not disappeared into the void (rather, it is accessible via the fields of the CompositeException that was raised).

Of course, synchronous errors are propagated as well:

julia> @sync begin
           error("synchronous error")
       end
ERROR: synchronous error
Stacktrace:
 [1] error(::String) at ./error.jl:33
 [2] top-level scope at task.jl:244

In this case sync_end does not appear in the stacktrace, as control never reached the call to sync_end at the end of the @sync call. Rather, control was interrupted by the error in the middle of executing the sync block.

However, I’ve noticed that, when a synchronous error occurs within a @sync block, any asynchronous errors that occur are not caught by the call to sync_end:

julia> @sync begin
           t = @async begin
               error("asynchronous error")
           end
           sleep(1)
           error("synchronous error")
       end
ERROR: synchronous error
Stacktrace:
 [1] error(::String) at ./error.jl:33
 [2] top-level scope at task.jl:244

The asynchronous error is not caught, and no CompositeException is raised, as control never reaches the call to sync_end. The call to sleep(1) gives the @async task t time to execute, so the asynchronous error probably occurs before the synchronous error. But the asynchronous error never propagates to the user.

I feel that something is left to be desiered, because the @sync block can handle either 1) catching and propagating multiple @async errors, or 2) catching and propagating a synchronous error, but it is not able to simultaneously handle asynchronous and synchronous errors.

Should this be considered a bug?

On second thought, a @sync macro that collects and rethrows simultaneous synchronous and asynchronous errors would probably be undesirable, as the macro would not be able to immediately propagate the synchronous exceptions that occur; rather it would have to wait to collect any asynchronous exceptions.

I’ve realized that this topic is actually related to the discussions about structured concurrency a la nurseries.
E.g @schedule considered harmful