How to get backtrace of `@async` Task

I’m unsure how to get the thrown error or backtrace of an asynchronous Task.

Here’s an example script:

# silent_test_all_in_one.jl

function do_some_work(increase::Real)
    a = 0.0
    start_time = time()
    while time() - start_time < 5
        b += increase
        sleep(0.1)
    end
    a
end

function start_blocking()
    println("start() is waiting for do_some_work() to be done")
    do_some_work(10)
end

function start_async()
    start_time = time()
    t = @async do_some_work(10)
    while !istaskdone(t)
        println("start() is waiting for do_some_work() to be done")
        sleep(1)
    end
end

To test, I did the following

  1. Save that file as “silent_test_all_in_one.jl” and include it at the REPL
  2. call start_blocking and see the appropriate error
  3. call start_async and don’t see any indication of an error
julia> include("silent_test_all_in_one.jl")
start_async (generic function with 1 method)

julia> start_blocking()
start() is waiting for do_some_work() to be done
ERROR: UndefVarError: b not defined
Stacktrace:
 [1] do_some_work at C:\Users\dave\Documents\silent_test_all_in_one.jl:7 [inlined]
 [2] start_blocking() at C:\Users\dave\Documents\silent_test_all_in_one.jl:15
 [3] top-level scope at none:0

julia> start_async()
start() is waiting for do_some_work() to be done

julia> versioninfo()
Julia Version 1.0.2
Commit d789231e99 (2018-11-08 20:11 UTC)
Platform Info:
  OS: Windows (x86_64-w64-mingw32)
  CPU: Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-6.0.0 (ORCJIT, haswell)

Is this expected? Can this be avoided?

I’m working on a project that uses HTTP.jl to handle some requests asynchronously with Tasks. Often, the code “hangs” in the REPL and gives no feedback and that’s when I realized that asynchronous Taskss don’t seem to print out thrown errors or backtraces when the code actually errors out.

1 Like

You can either print the error yourself or look at what the task resolves to:

function start_async_print()
    start_time = time()
    t = @async try
        do_some_work(10)
    catch err
        bt = catch_backtrace()
        println()
        showerror(stderr, err, bt)
    end
    while !istaskdone(t)
        println("start() is waiting for do_some_work() to be done")
        sleep(1)
    end
end

function start_async_throw()
    start_time = time()
    t = do_some_work(10)
    while !istaskdone(t)
        println("start() is waiting for do_some_work() to be done")
        sleep(1)
    end
    wait(t) # or fetch(t)
end

gives

julia> start_async_print()
start() is waiting for do_some_work() to be done

UndefVarError: b not defined
Stacktrace:
 [1] do_some_work at ./untitled-8822451613b5304f5f50924dadde5d27:6 [inlined]
 [2] (::getfield(Main, Symbol("##27#28")))() at ./task.jl:259
julia> start_async_throw()
ERROR: UndefVarError: b not defined
Stacktrace:
 [1] do_some_work at ./untitled-8822451613b5304f5f50924dadde5d27:6 [inlined]
 [2] start_async_throw() at ./untitled-8822451613b5304f5f50924dadde5d27:33
 [3] top-level scope at none:0

The wait/fetch approach obviously doesn’t work very well when you don’t want to block.

1 Like

Thank you! Is there a good way to encapsulate that behavior in a new macro? This seems to work:

macro async_showerr(ex)
    quote
        t = @async try
            eval($(esc(ex)))
        catch err
            bt = catch_backtrace()
            println()
            showerror(stderr, err, bt)
        end
    end
end

But is there a downside to using a macro like this?

3 Likes

@pfitzseb I’m curious why @async doesn’t do this automatically and I’m trying to imagine the downsides.

This seems like an easily avoidable trap for newbies. For more advanced users who want the current behavior explicitly, they could call a macro like @async_silent.

Does that make sense?

2 Likes

This is an old topic, but I have the very same issue with async tasks: they don’t show a useful stacktrace be default. Not only @async itself, but also e.g. asyncmap:

asyncmap(0:10) do i
    1 ÷ i
end

ERROR: DivideError: integer division error
Stacktrace:
 [1] (::Base.var"#770#772")(::Task) at ./asyncmap.jl:178
 [2] foreach(::Base.var"#770#772", ::Array{Any,1}) at ./abstractarray.jl:2009
 [3] maptwice(::Function, ::Channel{Any}, ::Array{Any,1}, ::UnitRange{Int64}) at ./asyncmap.jl:178
 [4] wrap_n_exec_twice at ./asyncmap.jl:154 [inlined]
 [5] #async_usemap#755 at ./asyncmap.jl:103 [inlined]
 [6] #asyncmap#754 at ./asyncmap.jl:81 [inlined]
 [7] asyncmap(::Function, ::UnitRange{Int64}) at ./asyncmap.jl:81
 [8] top-level scope at REPL[3]:1

compare to map:

map(0:10) do i
    1 ÷ i
end

ERROR: DivideError: integer division error
Stacktrace:
 [1] div at ./int.jl:260 [inlined]
 [2] #13 at ./REPL[4]:2 [inlined]
 [3] iterate at ./generator.jl:47 [inlined]
 [4] _collect(::UnitRange{Int64}, ::Base.Generator{UnitRange{Int64},var"#13#14"}, ::Base.EltypeUnknown, ::Base.HasShape{1}) at ./array.jl:699
 [5] collect_similar(::UnitRange{Int64}, ::Base.Generator{UnitRange{Int64},var"#13#14"}) at ./array.jl:628
 [6] map(::Function, ::UnitRange{Int64}) at ./abstractarray.jl:2162
 [7] top-level scope at REPL[4]:1

Is there any way to force the stacktrace go further than the asyncmap internals, i.e. to the actual user function? I can only imagine putting try ... catch ... end there manually.

3 Likes

Wondering in the same thing; I very much got snared in this newbie trap.

1 Like

There’s an errormonitor function to print errors thrown in an @async task.

julia> errormonitor(@async (sleep(1.0); error("bla")))
Task (runnable) @0x00007fbe2ae5bb70

julia> Unhandled Task ERROR: bla
Stacktrace:
 [1] error(s::String)
2 Likes