Rethrow error from @async in macro

I can’t figure out how to properly rethrow an error when using a macro + @async.

This is related to Rethrow error from @async and How to get backtrace of `@async` Task - #2 by pfitzseb

import Base.Threads: @spawn

macro async_showerr(ex)
    quote
        @async try
            eval(esc(ex))
        catch err
            rethrow()
        end
    end
end

function bad_func2(i)
    try
        error("error from bad_func2")
    catch err
        rethrow()
    end
end

function bad_func(i)
    error("error from bad_func")
end

function main()
    is = collect(1:3)
    while true
        println("looping")
        @sync for i in is
            # @async bad_func(i) # shows error & halts
            # @async bad_func2(i) # shows error & halts
            @async_showerr bad_func(i) # no error, causes infinite loop. Why?
        end
        sleep(0.5)
    end
end

main()

Any ideas how to throw the error & halt?

Your macro is wrong, ex is not used at all.

macro async_showerr(ex)
    quote
        @async try
            eval(esc(ex))
        catch err
            rethrow()
        end
    end
end

I think the macro has hygiene problem. @sync will create a local channel named sync and @async will push task to this local channel. This relies on the escaped symbol sync (otherwise, they may refer to different variable with the same name). However, your macro async_showerr pollutes the scope and cause the mismatch of these two symbols. We can check this by calling @code_lowered main():

julia> @code_lowered main()
CodeInfo(
1 ─ %1  = 1:3
└──       is = Main.collect(%1)
2 ┄       goto #9 if not true
3 ─       Main.println("looping")
│   %5  = Base.Channel(Base.Inf)
│         Core.NewvarNode(:(v))
│         sync#41 = %5
│   %8  = is
│         @_3 = Base.iterate(%8)
│   %10 = @_3 === nothing
│   %11 = Base.not_int(%10)
└──       goto #8 if not %11
4 ┄ %13 = @_3
│         i = Core.getfield(%13, 1)
│   %15 = Core.getfield(%13, 2)
│         #1 = %new(Main.:(var"#1#2"))
│   %17 = #1
│         task = Base.Task(%17)
└──       goto #6 if not false
5 ─       Base.put!(Main.:(var"##sync#41"), task)
6 ┄       Base.schedule(task)
│         task
│         @_3 = Base.iterate(%8, %15)
│   %24 = @_3 === nothing
│   %25 = Base.not_int(%24)
└──       goto #8 if not %25
7 ─       goto #4
8 ┄       v = nothing
│         Base.sync_end(sync#41)
│         v
│         Main.sleep(0.5)
└──       goto #2
9 ─       return nothing
)

So you can see two symbols sync#41 and Main.:(var"##sync#41"). One is local and the other refers to a symbol in Main. So Julia will not ever synchronize at all.

To solve this problem, we need to escape the whole quote expression and everything is fine:

macro async_showerr(ex)
    esc(quote
        @async try
            eval($ex)
        catch err
            rethrow()
        end
    end)
end

And we get:

julia> main()
looping
ERROR: TaskFailedException

    nested task error: error from bad_func
    Stacktrace:
     [1] error(s::String)
       @ Base ./error.jl:33
     [2] bad_func(i::Int64)
       @ Main ./REPL[18]:2
     [3] (::var"#5#6"{Int64})()
       @ Main ./task.jl:411

...and 2 more exceptions.

Stacktrace:
 [1] sync_end(c::Channel{Any})
   @ Base ./task.jl:369
 [2] macro expansion
   @ ./task.jl:388 [inlined]
 [3] main()
   @ Main ./REPL[19]:5
 [4] top-level scope
   @ REPL[20]:1

Wow, what a phenomenal answer!! Not only did you fix it, but showed me how to figure this out myself. Thank you so much.