diff --git a/NEWS.md b/NEWS.md index 664d8f615dbc6..6d99c54b40504 100644 --- a/NEWS.md +++ b/NEWS.md @@ -52,6 +52,9 @@ Standard library changes * Sets are now displayed less compactly in the REPL, as a column of elements, like vectors and dictionaries ([#33300]). +* The experimental function `Base.catch_stack()` has been renamed to + `current_exceptions()` and is now exported from Base. It has also been given + a more precise return type. #### Libdl diff --git a/base/client.jl b/base/client.jl index a6b07aadde5a7..b678b69320955 100644 --- a/base/client.jl +++ b/base/client.jl @@ -98,12 +98,12 @@ function display_error(io::IO, er, bt) showerror(IOContext(io, :limit => true), er, bt, backtrace = bt!==nothing) println(io) end -function display_error(io::IO, stack::Vector) +function display_error(io::IO, stack::ExceptionStack) printstyled(io, "ERROR: "; bold=true, color=Base.error_color()) bt = Any[ (x[1], scrub_repl_backtrace(x[2])) for x in stack ] show_exception_stack(IOContext(io, :limit => true), bt) end -display_error(stack::Vector) = display_error(stderr, stack) +display_error(stack::ExceptionStack) = display_error(stderr, stack) display_error(er, bt=nothing) = display_error(stderr, er, bt) function eval_user_input(errio, @nospecialize(ast), show_value::Bool) @@ -141,7 +141,7 @@ function eval_user_input(errio, @nospecialize(ast), show_value::Bool) @error "SYSTEM: display_error(errio, lasterr) caused an error" end errcount += 1 - lasterr = catch_stack() + lasterr = current_exceptions() if errcount > 2 @error "It is likely that something important is broken, and Julia will not be able to continue normally" errcount break @@ -287,7 +287,7 @@ function exec_options(opts) try include(Main, PROGRAM_FILE) catch - invokelatest(display_error, catch_stack()) + invokelatest(display_error, current_exceptions()) if !is_interactive exit(1) end @@ -483,7 +483,7 @@ function _start() try exec_options(JLOptions()) catch - invokelatest(display_error, catch_stack()) + invokelatest(display_error, current_exceptions()) exit(1) end if is_interactive && have_color diff --git a/base/deprecated.jl b/base/deprecated.jl index fd8e42c46427b..f7f370691271e 100644 --- a/base/deprecated.jl +++ b/base/deprecated.jl @@ -191,4 +191,6 @@ MPFR.BigFloat(x::Real, prec::Int, rounding::RoundingMode) = BigFloat(x, rounding Base.@deprecate_binding Mutex ReentrantLock end +@deprecate catch_stack(task=current_task(); include_bt=true) current_exceptions(task; backtrace=include_bt) false + # END 1.3 deprecations diff --git a/base/error.jl b/base/error.jl index 604914318c6b1..049f0de740c2f 100644 --- a/base/error.jl +++ b/base/error.jl @@ -54,7 +54,7 @@ exception will continue propagation as if it had not been caught. the program state at the time of the error so you're encouraged to instead throw a new exception using `throw(e)`. In Julia 1.1 and above, using `throw(e)` will preserve the root cause exception on the stack, as - described in [`catch_stack`](@ref). + described in [`current_exceptions`](@ref). """ rethrow() = ccall(:jl_rethrow, Bottom, ()) rethrow(e) = ccall(:jl_rethrow_other, Bottom, (Any,), e) @@ -123,32 +123,38 @@ function catch_backtrace() return _reformat_bt(bt::Vector{Ptr{Cvoid}}, bt2::Vector{Any}) end +struct ExceptionStack <: AbstractArray{Any,1} + stack +end + """ - catch_stack(task=current_task(); [inclue_bt=true]) + current_exceptions(task=current_task(); [inclue_bt=true]) Get the stack of exceptions currently being handled. For nested catch blocks there may be more than one current exception in which case the most recently -thrown exception is last in the stack. The stack is returned as a Vector of -`(exception,backtrace)` pairs, or a Vector of exceptions if `include_bt` is -false. +thrown exception is last in the stack. The stack is returned as an +`ExceptionStack` which is an AbstractVector of named tuples +`(exception,backtrace)`. If `backtrace` is false, the backtrace in each pair +will be set to `nothing`. Explicitly passing `task` will return the current exception stack on an arbitrary task. This is useful for inspecting tasks which have failed due to uncaught exceptions. -!!! compat "Julia 1.1" - This function is experimental in Julia 1.1 and will likely be renamed in a - future release (see https://github.com/JuliaLang/julia/pull/29901). +!!! compat "Julia 1.4" + This function went by the experiemental name `catch_stack()` in Julia + 1.1–1.3, and had a plain Vector as a return type. """ -function catch_stack(task=current_task(); include_bt=true) - raw = ccall(:jl_get_excstack, Any, (Any,Cint,Cint), task, include_bt, typemax(Cint)) +function current_exceptions(task=current_task(); backtrace=true) + raw = ccall(:jl_get_excstack, Any, (Any,Cint,Cint), task, backtrace, typemax(Cint)) formatted = Any[] - stride = include_bt ? 3 : 1 + stride = backtrace ? 3 : 1 for i = reverse(1:stride:length(raw)) - e = raw[i] - push!(formatted, include_bt ? (e,Base._reformat_bt(raw[i+1],raw[i+2])) : e) + exc = raw[i] + bt = backtrace ? Base._reformat_bt(raw[i+1],raw[i+2]) : nothing + push!(formatted, (exception=exc,backtrace=bt)) end - formatted + ExceptionStack(formatted) end ## keyword arg lowering generates calls to this ## diff --git a/base/errorshow.jl b/base/errorshow.jl index 4cb0b045a784a..2e0495b0bc846 100644 --- a/base/errorshow.jl +++ b/base/errorshow.jl @@ -692,7 +692,7 @@ function process_backtrace(t::Vector, limit::Int=typemax(Int); skipC = true) return ret end -function show_exception_stack(io::IO, stack::Vector) +function show_exception_stack(io::IO, stack) # Display exception stack with the top of the stack first. This ordering # means that the user doesn't have to scroll up in the REPL to discover the # root cause. @@ -717,3 +717,13 @@ function show(io::IO, ip::InterpreterIP) end end +size(s::ExceptionStack) = size(s.stack) +getindex(s::ExceptionStack, i::Int) = s.stack[i] + +function show(io::IO, ::MIME"text/plain", stack::ExceptionStack) + nexc = length(stack) + printstyled(io, nexc, "-element ExceptionStack", nexc == 0 ? "" : ":\n") + show_exception_stack(io, stack) +end +show(io::IO, stack::ExceptionStack) = show(io, MIME("text/plain"), stack) + diff --git a/base/exports.jl b/base/exports.jl index 90e177fc8a9f7..2dec0b442c9e5 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -689,6 +689,7 @@ export # errors backtrace, catch_backtrace, + current_exceptions, error, rethrow, retry, diff --git a/doc/src/base/base.md b/doc/src/base/base.md index 9dc9f651be80d..74039b9be70bb 100644 --- a/doc/src/base/base.md +++ b/doc/src/base/base.md @@ -330,7 +330,7 @@ Core.throw Base.rethrow Base.backtrace Base.catch_backtrace -Base.catch_stack +Base.current_exceptions Base.@assert Base.ArgumentError Base.AssertionError diff --git a/doc/src/manual/control-flow.md b/doc/src/manual/control-flow.md index 986760d7934ec..f3b8ea3694911 100644 --- a/doc/src/manual/control-flow.md +++ b/doc/src/manual/control-flow.md @@ -792,7 +792,7 @@ The power of the `try/catch` construct lies in the ability to unwind a deeply ne immediately to a much higher level in the stack of calling functions. There are situations where no error has occurred, but the ability to unwind the stack and pass a value to a higher level is desirable. Julia provides the [`rethrow`](@ref), [`backtrace`](@ref), [`catch_backtrace`](@ref) -and [`Base.catch_stack`](@ref) functions for more advanced error handling. +and [`current_exceptions`](@ref) functions for more advanced error handling. ### `finally` Clauses diff --git a/doc/src/manual/stacktraces.md b/doc/src/manual/stacktraces.md index 23cad109182df..32334d27ac367 100644 --- a/doc/src/manual/stacktraces.md +++ b/doc/src/manual/stacktraces.md @@ -187,7 +187,7 @@ ERROR: Whoops! [...] ``` -## Exception stacks and `catch_stack` +## Exception stacks and [`current_exceptions`](@ref) !!! compat "Julia 1.1" Exception stacks requires at least Julia 1.1. @@ -197,7 +197,7 @@ identify the root cause of a problem. The julia runtime supports this by pushing *exception stack* as it occurs. When the code exits a `catch` normally, any exceptions which were pushed onto the stack in the associated `try` are considered to be successfully handled and are removed from the stack. -The stack of current exceptions can be accessed using the experimental [`Base.catch_stack`](@ref) function. For example, +The stack of current exceptions can be accessed using the [`current_exceptions`](@ref) function. For example, ```julia-repl julia> try @@ -206,7 +206,7 @@ julia> try try error("(B) An exception while handling the exception") catch - for (exc, bt) in Base.catch_stack() + for (exc, bt) in current_exceptions() showerror(stdout, exc, bt) println() end @@ -235,7 +235,7 @@ exiting both catch blocks normally (i.e., without throwing a further exception) and are no longer accessible. The exception stack is stored on the `Task` where the exceptions occurred. When a task fails with uncaught exceptions, -`catch_stack(task)` may be used to inspect the exception stack for that task. +`current_exceptions(task)` may be used to inspect the exception stack for that task. ## Comparison with [`backtrace`](@ref) diff --git a/src/stackwalk.c b/src/stackwalk.c index f088f1b7b69a2..bce15b92202a9 100644 --- a/src/stackwalk.c +++ b/src/stackwalk.c @@ -296,7 +296,7 @@ JL_DLLEXPORT jl_value_t *jl_get_backtrace(void) // interleaved. JL_DLLEXPORT jl_value_t *jl_get_excstack(jl_task_t* task, int include_bt, int max_entries) { - JL_TYPECHK(catch_stack, task, (jl_value_t*)task); + JL_TYPECHK(current_exceptions, task, (jl_value_t*)task); jl_ptls_t ptls = jl_get_ptls_states(); if (task != ptls->current_task && task->state != failed_sym && task->state != done_sym) { diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index 514ce37c14400..c1aac8ca811e1 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -16,8 +16,7 @@ import Base: display, show, AnyDict, - ==, - catch_stack + == include("Terminals.jl") @@ -95,7 +94,7 @@ function eval_user_input(@nospecialize(ast), backend::REPLBackend) println("SYSTEM ERROR: Failed to report error to REPL frontend") println(err) end - lasterr = catch_stack() + lasterr = current_exceptions() end end Base.sigatomic_end() @@ -174,7 +173,7 @@ function print_response(errio::IO, @nospecialize(response), show_value::Bool, ha println(errio) # an error during printing is likely to leave us mid-line println(errio, "SYSTEM (REPL): showing an error caused an error") try - Base.invokelatest(Base.display_error, errio, catch_stack()) + Base.invokelatest(Base.display_error, errio, current_exceptions()) catch e # at this point, only print the name of the type as a Symbol to # minimize the possibility of further errors. @@ -184,7 +183,7 @@ function print_response(errio::IO, @nospecialize(response), show_value::Bool, ha end break end - val = catch_stack() + val = current_exceptions() iserr = true end end @@ -724,7 +723,7 @@ function respond(f, repl, main; pass_empty = false) ast = Base.invokelatest(f, line) response = eval_with_backend(ast, backend(repl)) catch - response = (catch_stack(), true) + response = (current_exceptions(), true) end print_response(repl, response, !ends_with_semicolon(line), Base.have_color) end @@ -873,7 +872,7 @@ function setup_interface( end hist_from_file(hp, f, hist_path) catch - print_response(repl, (catch_stack(),true), true, Base.have_color) + print_response(repl, (current_exceptions(),true), true, Base.have_color) println(outstream(repl)) @info "Disabling history file for this session" repl.history_file = false diff --git a/stdlib/REPL/test/repl.jl b/stdlib/REPL/test/repl.jl index b4c08dc821317..06cd6045c1be8 100644 --- a/stdlib/REPL/test/repl.jl +++ b/stdlib/REPL/test/repl.jl @@ -819,7 +819,7 @@ mutable struct Error19864 <: Exception; end function test19864() @eval Base.showerror(io::IO, e::Error19864) = print(io, "correct19864") buf = IOBuffer() - fake_response = (Any[(Error19864(), Ptr{Cvoid}[])], true) + fake_response = (Base.ExceptionStack([(exception=Error19864(),backtrace=Ptr{Cvoid}[])]),true) REPL.print_response(buf, fake_response, false, false, nothing) return String(take!(buf)) end diff --git a/stdlib/Test/src/Test.jl b/stdlib/Test/src/Test.jl index 8c515b3330532..44923f9df438a 100644 --- a/stdlib/Test/src/Test.jl +++ b/stdlib/Test/src/Test.jl @@ -484,7 +484,7 @@ function get_test_result(ex, source) $testret catch _e _e isa InterruptException && rethrow() - Threw(_e, Base.catch_stack(), $(QuoteNode(source))) + Threw(_e, Base.current_exceptions(), $(QuoteNode(source))) end end Base.remove_linenums!(result) @@ -1118,7 +1118,7 @@ function testset_beginend(args, tests, source) err isa InterruptException && rethrow() # something in the test block threw an error. Count that as an # error in this test set - record(ts, Error(:nontest_error, Expr(:tuple), err, Base.catch_stack(), $(QuoteNode(source)))) + record(ts, Error(:nontest_error, Expr(:tuple), err, Base.current_exceptions(), $(QuoteNode(source)))) finally copy!(RNG, oldrng) end @@ -1191,7 +1191,7 @@ function testset_forloop(args, testloop, source) err isa InterruptException && rethrow() # Something in the test block threw an error. Count that as an # error in this test set - record(ts, Error(:nontest_error, Expr(:tuple), err, Base.catch_stack(), $(QuoteNode(source)))) + record(ts, Error(:nontest_error, Expr(:tuple), err, Base.current_exceptions(), $(QuoteNode(source)))) end end quote diff --git a/stdlib/Test/src/logging.jl b/stdlib/Test/src/logging.jl index 7a254a80b9038..982936aa4490b 100644 --- a/stdlib/Test/src/logging.jl +++ b/stdlib/Test/src/logging.jl @@ -174,7 +174,7 @@ macro test_logs(exs...) $(QuoteNode(exs[1:end-1])), logs) end catch e - testres = Error(:test_error, $orig_expr, e, Base.catch_stack(), $sourceloc) + testres = Error(:test_error, $orig_expr, e, Base.current_exceptions(), $sourceloc) end Test.record(Test.get_testset(), testres) value diff --git a/test/client.jl b/test/client.jl index 37516fb00ea91..ad43196431896 100644 --- a/test/client.jl +++ b/test/client.jl @@ -21,7 +21,7 @@ nested_error_pattern = r""" err_str = try eval(nested_error_expr) catch - excs = Base.catch_stack() + excs = Base.current_exceptions() @test typeof.(first.(excs)) == [UndefVarError, DivideError] sprint(Base.display_error, excs) end diff --git a/test/exceptions.jl b/test/exceptions.jl index e47862c8312d1..9dc24b8b06341 100644 --- a/test/exceptions.jl +++ b/test/exceptions.jl @@ -1,52 +1,51 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license using Test -using Base: catch_stack @testset "Basic exception stack handling" begin # Exiting the catch block normally pops the exception try error("A") catch - @test length(catch_stack()) == 1 + @test length(current_exceptions()) == 1 end - @test length(catch_stack()) == 0 + @test length(current_exceptions()) == 0 # Exiting via a finally block does not pop the exception try try error("A") finally - @test length(catch_stack()) == 1 + @test length(current_exceptions()) == 1 end catch - @test length(catch_stack()) == 1 + @test length(current_exceptions()) == 1 end # The combined try-catch-finally form obeys the same rules as above try error("A") catch - @test length(catch_stack()) == 1 + @test length(current_exceptions()) == 1 finally - @test length(catch_stack()) == 0 + @test length(current_exceptions()) == 0 end - @test length(catch_stack()) == 0 + @test length(current_exceptions()) == 0 # Errors are pushed onto the stack according to catch block nesting try error("RootCause") catch - @test length(catch_stack()) == 1 + @test length(current_exceptions()) == 1 try error("B") catch - stack = catch_stack() + stack = current_exceptions() @test length(stack) == 2 - @test stack[1][1].msg == "RootCause" - @test stack[2][1].msg == "B" + @test stack[1].exception.msg == "RootCause" + @test stack[2].exception.msg == "B" end # Stack pops correctly - stack = catch_stack() + stack = current_exceptions() @test length(stack) == 1 - @test stack[1][1].msg == "RootCause" + @test stack[1].exception.msg == "RootCause" end end @@ -55,7 +54,7 @@ end val = try error("A") catch - @test length(catch_stack()) == 1 + @test length(current_exceptions()) == 1 1 end @test val == 1 @@ -64,11 +63,11 @@ end try error("A") catch - length(catch_stack()) + length(current_exceptions()) end end @test test_exc_stack_tailpos() == 1 - @test length(catch_stack()) == 0 + @test length(current_exceptions()) == 0 end @testset "Exception stacks - early exit from try or catch" begin @@ -78,7 +77,7 @@ end try error("A") catch - @test length(catch_stack()) == 1 + @test length(current_exceptions()) == 1 return end end @@ -88,7 +87,7 @@ end try error("A") catch - @test length(catch_stack()) == 1 + @test length(current_exceptions()) == 1 break end end @@ -97,19 +96,19 @@ end try error("A") catch - @test length(catch_stack()) == 1 + @test length(current_exceptions()) == 1 break finally - @test length(catch_stack()) == 0 + @test length(current_exceptions()) == 0 end end - @test length(catch_stack()) == 0 + @test length(current_exceptions()) == 0 for i=1:1 try error("A") catch - @test length(catch_stack()) == 1 + @test length(current_exceptions()) == 1 continue end end @@ -117,38 +116,38 @@ end try error("A") catch - @test length(catch_stack()) == 1 + @test length(current_exceptions()) == 1 continue finally - @test length(catch_stack()) == 0 + @test length(current_exceptions()) == 0 end end - @test length(catch_stack()) == 0 + @test length(current_exceptions()) == 0 try error("A") catch - @test length(catch_stack()) == 1 + @test length(current_exceptions()) == 1 @goto outofcatch end @label outofcatch try error("A") catch - @test length(catch_stack()) == 1 + @test length(current_exceptions()) == 1 @goto outofcatch2 finally - @test length(catch_stack()) == 0 + @test length(current_exceptions()) == 0 end @label outofcatch2 - @test length(catch_stack()) == 0 + @test length(current_exceptions()) == 0 # Exiting from a try block in various ways should not affect the exception # stack state. try error("ExceptionInOuterTry") catch - @test length(catch_stack()) == 1 + @test length(current_exceptions()) == 1 function test_exc_stack_try_return() try return @@ -173,8 +172,8 @@ end catch end @label outoftry - @test length(catch_stack()) == 1 - @test catch_stack()[1][1] == ErrorException("ExceptionInOuterTry") + @test length(current_exceptions()) == 1 + @test current_exceptions()[1].exception == ErrorException("ExceptionInOuterTry") end end @@ -193,10 +192,10 @@ end @test try test_exc_stack_deep(100) catch - @test catch_stack()[1][1] == ErrorException("RootCause") - length(catch_stack()) + @test current_exceptions()[1].exception == ErrorException("RootCause") + length(current_exceptions()) end == 100 - @test length(catch_stack()) == 0 + @test length(current_exceptions()) == 0 end @testset "Exception stacks and Tasks" begin @@ -213,10 +212,10 @@ end @test t.state == :done @test t.result == ErrorException("B") # Task exception state is preserved around task switches - @test length(catch_stack()) == 1 - @test catch_stack()[1][1] == ErrorException("A") + @test length(current_exceptions()) == 1 + @test current_exceptions()[1].exception == ErrorException("A") end - @test length(catch_stack()) == 0 + @test length(current_exceptions()) == 0 # test rethrow() rethrows correct state bt = [] try @@ -239,7 +238,7 @@ end @test exc == ErrorException("A") @test bt == catch_backtrace() end - @test length(catch_stack()) == 0 + @test length(current_exceptions()) == 0 # test rethrow with argument bt = [] try @@ -261,7 +260,7 @@ end @test exc == ErrorException("C") @test bt == catch_backtrace() end - @test length(catch_stack()) == 0 + @test length(current_exceptions()) == 0 # Exception stacks on other tasks t = @task try error("A") @@ -271,7 +270,10 @@ end yield(t) @test t.state == :failed @test t.result == ErrorException("B") - @test catch_stack(t, include_bt=false) == [ErrorException("A"), ErrorException("B")] + @test current_exceptions(t, backtrace=false) == [ + (exception=ErrorException("A"),backtrace=nothing), + (exception=ErrorException("B"),backtrace=nothing) + ] # Exception stacks for tasks which never get the chance to start t = @task nothing @test (try @@ -280,12 +282,12 @@ end catch e e end).task.exception == ErrorException("expected") - @test length(catch_stack(t)) == 1 - @test length(catch_stack(t)[1][2]) > 0 # backtrace is nonempty + @test length(current_exceptions(t)) == 1 + @test length(current_exceptions(t)[1].backtrace) > 0 # backtrace is nonempty # Exception stacks should not be accessed on concurrently running tasks t = @task ()->nothing @test_throws ErrorException("Inspecting the exception stack of a task which might "* - "be running concurrently isn't allowed.") catch_stack(t) + "be running concurrently isn't allowed.") current_exceptions(t) end @testset "rethrow" begin