Skip to content

Commit

Permalink
Exception stack API refinements (#29901)
Browse files Browse the repository at this point in the history
* Rename the non-exported `catch_stack()` to the more descriptive name
  `current_exceptions()`. Keep the old name available but deprecated.

* Introduce an ExceptionStack as the return type for the function, which
  (as an AbstractVector) is API-compatible with the previous type
  returned by `catch_stack()`

Having ExceptionStack gives us a place to integrate exception printing
in a natural way. In the same way this should be useful for dispatch in
other areas of the ecosystem which want to dispatch on exception stacks.
  • Loading branch information
c42f committed May 15, 2021
1 parent 2dfe8d7 commit 9113c01
Show file tree
Hide file tree
Showing 18 changed files with 138 additions and 106 deletions.
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Standard library changes
```
([#39322])
* `@lock` is now exported from Base ([#39588]).
* The experimental function `Base.catch_stack()` has been renamed to `current_exceptions()`, exported from Base and given a more specific return type ([#29901])

#### Package Manager

Expand Down
12 changes: 6 additions & 6 deletions base/client.jl
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,13 @@ 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)
println(io)
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)
Expand Down Expand Up @@ -143,7 +143,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
Expand Down Expand Up @@ -257,7 +257,7 @@ function exec_options(opts)
try
load_julia_startup()
catch
invokelatest(display_error, catch_stack())
invokelatest(display_error, current_exceptions())
!(repl || is_interactive) && exit(1)
end
end
Expand Down Expand Up @@ -291,7 +291,7 @@ function exec_options(opts)
try
include(Main, PROGRAM_FILE)
catch
invokelatest(display_error, catch_stack())
invokelatest(display_error, current_exceptions())
if !is_interactive::Bool
exit(1)
end
Expand Down Expand Up @@ -494,7 +494,7 @@ function _start()
try
exec_options(JLOptions())
catch
invokelatest(display_error, catch_stack())
invokelatest(display_error, current_exceptions())
exit(1)
end
if is_interactive && get(stdout, :color, false)
Expand Down
3 changes: 3 additions & 0 deletions base/deprecated.jl
Original file line number Diff line number Diff line change
Expand Up @@ -250,4 +250,7 @@ cat_shape(dims, shape::Tuple{}) = () # make sure `cat_shape(dims, ())` do not re
return getfield(x, s)
end

# This function was marked as experimental and not exported.
@deprecate catch_stack(task=current_task(); include_bt=true) current_exceptions(task; backtrace=include_bt) false

# END 1.7 deprecations
34 changes: 20 additions & 14 deletions base/error.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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(@nospecialize(e)) = ccall(:jl_rethrow_other, Bottom, (Any,), e)
Expand Down Expand Up @@ -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.7"
This function went by the experiemental name `catch_stack()` in Julia
1.1–1.6, and had a plain Vector-of-tuples 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))::Vector{Any}
function current_exceptions(task=current_task(); backtrace=true)
raw = ccall(:jl_get_excstack, Any, (Any,Cint,Cint), task, backtrace, typemax(Cint))::Vector{Any}
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 ##
Expand Down
14 changes: 13 additions & 1 deletion base/errorshow.jl
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,7 @@ function process_backtrace(t::Vector, limit::Int=typemax(Int); skipC = true)
return _simplify_include_frames(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.
Expand Down Expand Up @@ -886,3 +886,15 @@ function noncallable_number_hint_handler(io, ex, arg_types, kwargs)
end

Experimental.register_error_hint(noncallable_number_hint_handler, MethodError)

# ExceptionStack implementation
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)

1 change: 1 addition & 0 deletions base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,7 @@ export
# errors
backtrace,
catch_backtrace,
current_exceptions,
error,
rethrow,
retry,
Expand Down
10 changes: 5 additions & 5 deletions base/task.jl
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ function showerror(io::IO, ex::TaskFailedException, bt = nothing; backtrace=true
end

function show_task_exception(io::IO, t::Task; indent = true)
stack = catch_stack(t)
stack = current_exceptions(t)
b = IOBuffer()
if isempty(stack)
# exception stack buffer not available; probably a serialized task
Expand Down Expand Up @@ -162,7 +162,7 @@ end
end
elseif field === :backtrace
# TODO: this field name should be deprecated in 2.0
return catch_stack(t)[end][2]
return current_exceptions(t)[end][2]
elseif field === :exception
# TODO: this field name should be deprecated in 2.0
return t._isexception ? t.result : nothing
Expand Down Expand Up @@ -434,18 +434,18 @@ function errormonitor(t::Task)
try # try to display the failure atomically
errio = IOContext(PipeBuffer(), errs::IO)
emphasize(errio, "Unhandled Task ")
display_error(errio, catch_stack(t))
display_error(errio, current_exceptions(t))
write(errs, errio)
catch
try # try to display the secondary error atomically
errio = IOContext(PipeBuffer(), errs::IO)
print(errio, "\nSYSTEM: caught exception while trying to print a failed Task notice: ")
display_error(errio, catch_stack())
display_error(errio, current_exceptions())
write(errs, errio)
flush(errs)
# and then the actual error, as best we can
Core.print(Core.stderr, "while handling: ")
Core.println(Core.stderr, catch_stack(t)[end][1])
Core.println(Core.stderr, current_exceptions(t)[end][1])
catch e
# give up
Core.print(Core.stderr, "\nSYSTEM: caught exception of type ", typeof(e).name.name,
Expand Down
2 changes: 1 addition & 1 deletion doc/src/base/base.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ Core.throw
Base.rethrow
Base.backtrace
Base.catch_backtrace
Base.catch_stack
Base.current_exceptions
Base.@assert
Base.Experimental.register_error_hint
Base.Experimental.show_error_hints
Expand Down
2 changes: 1 addition & 1 deletion doc/src/manual/control-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -817,7 +817,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

Expand Down
8 changes: 4 additions & 4 deletions doc/src/manual/stacktraces.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,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.
Expand All @@ -195,7 +195,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
Expand All @@ -204,7 +204,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(stdout)
end
Expand Down Expand Up @@ -233,7 +233,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)

Expand Down
2 changes: 1 addition & 1 deletion src/stackwalk.c
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,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 == JL_TASK_STATE_RUNNABLE) {
jl_error("Inspecting the exception stack of a task which might "
Expand Down
13 changes: 6 additions & 7 deletions stdlib/REPL/src/REPL.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ import Base:
display,
show,
AnyDict,
==,
catch_stack
==

_displaysize(io::IO) = displaysize(io)::Tuple{Int,Int}

Expand Down Expand Up @@ -160,7 +159,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()
Expand Down Expand Up @@ -301,7 +300,7 @@ function print_response(errio::IO, response, show_value::Bool, have_color::Bool,
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.
Expand All @@ -311,7 +310,7 @@ function print_response(errio::IO, response, show_value::Bool, have_color::Bool,
end
break
end
val = catch_stack()
val = current_exceptions()
iserr = true
end
end
Expand Down Expand Up @@ -835,7 +834,7 @@ function respond(f, repl, main; pass_empty::Bool = false, suppress_on_semicolon:
ast = Base.invokelatest(f, line)
response = eval_with_backend(ast, backend(repl))
catch
response = Pair{Any, Bool}(catch_stack(), true)
response = Pair{Any, Bool}(current_exceptions(), true)
end
hide_output = suppress_on_semicolon && ends_with_semicolon(line)
print_response(repl, response, !hide_output, hascolor(repl))
Expand Down Expand Up @@ -987,7 +986,7 @@ function setup_interface(
hist_from_file(hp, hist_path)
catch
# use REPL.hascolor to avoid using the local variable with the same name
print_response(repl, Pair{Any, Bool}(catch_stack(), true), true, REPL.hascolor(repl))
print_response(repl, Pair{Any, Bool}(current_exceptions(), true), true, REPL.hascolor(repl))
println(outstream(repl))
@info "Disabling history file for this session"
repl.history_file = false
Expand Down
2 changes: 1 addition & 1 deletion stdlib/REPL/test/repl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -877,7 +877,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
Expand Down
4 changes: 2 additions & 2 deletions stdlib/Serialization/src/Serialization.jl
Original file line number Diff line number Diff line change
Expand Up @@ -462,11 +462,11 @@ function serialize(s::AbstractSerializer, t::Task)
serialize(s, t.code)
serialize(s, t.storage)
serialize(s, t.state)
if t._isexception && (stk = Base.catch_stack(t); !isempty(stk))
if t._isexception && (stk = Base.current_exceptions(t); !isempty(stk))
# the exception stack field is hidden inside the task, so if there
# is any information there make a CapturedException from it instead.
# TODO: Handle full exception chain, not just the first one.
serialize(s, CapturedException(stk[1][1], stk[1][2]))
serialize(s, CapturedException(stk[1].exception, stk[1].backtrace))
else
serialize(s, t.result)
end
Expand Down
6 changes: 3 additions & 3 deletions stdlib/Test/src/Test.jl
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,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)
Expand Down Expand Up @@ -1272,7 +1272,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)
pop_testset()
Expand Down Expand Up @@ -1346,7 +1346,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
Expand Down
2 changes: 1 addition & 1 deletion stdlib/Test/src/logging.jl
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,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
Expand Down
Loading

2 comments on commit 9113c01

@nanosoldier
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Executing the daily package evaluation, I will reply here when finished:

@nanosoldier runtests(ALL, isdaily = true)

@nanosoldier
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your package evaluation job has completed - possible new issues were detected. A full report can be found here. cc @maleadt

Please sign in to comment.