Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refine exception stack API #29901

Merged
merged 1 commit into from
May 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}
Copy link
Member

Choose a reason for hiding this comment

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

This type doesn't seem necessary to me. Since showing errors goes through display_error anyway, having a type with a show method is a bit redundant (and does have some duplicated code here).

Copy link
Member Author

@c42f c42f Dec 4, 2018

Choose a reason for hiding this comment

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

Since showing errors goes through display_error anyway

That's only true for the REPL where we pass exceptions and backtraces directly to display_error rather than dispatching them to some generic machinery which needs to match on their type.

In the generic case, there's currently no nice way to pass around a package of exceptions and backtraces together such that their type can be dispatched on in a non-clumsy way. I'm thinking particularly of Loggers, where I'd like to be able to have things like:

@error "Something bad happened" arbitrary_key_name=current_exceptions()

and have this actually display something sensible without clumsy matching of types in the Logger backend. We currently resort to such matching, for example

# Formatting of values in key value pairs
showvalue(io, msg) = show(io, "text/plain", msg)
function showvalue(io, e::Tuple{Exception,Any})
ex,bt = e
showerror(io, ex, bt; backtrace = bt!=nothing)
end
showvalue(io, ex::Exception) = showerror(io, ex)

Copy link
Member Author

Choose a reason for hiding this comment

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

Not that I love ExceptionStack as a type, it seems annoying and superfluous on the surface, and it's one of the main reasons I wanted some more eyes on this.

What I'd really like are suggestions about how to replace or improve it, while also having a way to pass these to logging macros and have them robustly recognized by the logger backend.

Copy link
Member Author

@c42f c42f Dec 4, 2018

Choose a reason for hiding this comment

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

It's true that there's a little duplication of code between show and display_error. Previously I tried making display_error dispatch to show and adding some IOContext handling to turn on the simplifications we currently use in display_error. That turned out somewhat complex though so I punted in the current iteration and left it with two concrete implementations.

I could give it another go.

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