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

Refine exception stack API #29901

merged 1 commit into from
May 15, 2021

Conversation

c42f
Copy link
Member

@c42f c42f commented Nov 2, 2018

Updated Description

This PR has been simplified with some parts merged separately (#30900, #30899). However, the catch_stack API has not been finalized. In the current version (as of 2019-02-06), I've got two things here:

  • Rename catch_stack() -> current_exceptions() as this seems more descriptive.
  • Introduce a new type ExceptionStack so that we can have a proper exception-and-backtrace specific type we can dispatch on. For example, this allows us to have a show implemented for exception and backtrace data which pretty prints the information in a much more readable way than the simple Vector of tuples that catch_stack returns right now.

Original Description

Here's a few refinements to the API introduced in #28878. I'm not quite happy with that API for various reasons so I want to revisit it before 1.1 feature freeze (or unexport it before 1.1).

IMO this is a big improvement, but still a bit WIP so feedback would be great.

  • Rename catch_stack() -> current_exceptions() as this seems far more transparent, and is in fact how the function is summarized in the docs. I wasn't happy with the previous name, it's got an analogy to catch_backtrace but other than that seemes too obscure. Alternative name proposals gladly welcome (the current_exceptions(task) variant is perhaps less natural).

  • Introduce a new type ExceptionStack so that we have a proper exception-and-backtrace specific type we can dispatch on. This allows us to treat showerror as more of an implementation detail, and simply override show(::IO,::ExceptionStack) in the usual way to get integrated pretty printing of exceptions and backtraces. I'm not quite certain about how this is a wrapper for a Vector; perhaps it should be an AbstractVector.

  • For PARTR sanity, prevent reading of current exceptions of a concurrently running task, at least for now.

  • Pretty printing of ExceptionStack in the REPL by overloading display_error. Could go into a separate PR if people want this simplified (or just review the commit 074e39b in isolation).

  • Pretty printing of ExceptionStack via show(). Examples follow:

try
    1 + [1,2]
catch exc
    try
        1÷0
    catch
        show(current_exceptions())
    end
end

excstack_show

Notice that by having proper show integration, we automatically get things like the following:

try
    1 + [1,2]
catch exc
    try
        1÷0
    catch
        @info "Ignoring an error" exception=current_exceptions()
    end
end

excstack_logging

@c42f c42f mentioned this pull request Nov 2, 2018
11 tasks
@c42f c42f added the domain:error handling Handling of exceptions by Julia or the user label Nov 2, 2018
@c42f c42f added this to the 1.1 milestone Nov 2, 2018
@c42f
Copy link
Member Author

c42f commented Nov 3, 2018

Regarding ExceptionStack, we could alternatively model this as

struct ExceptionState
    exception
    backtrace
end

const ExceptionStack = Vector{ExceptionState}

which might make more or less sense. It's a little nicer because it doesn't introduce a container type for this one small thing. It's a little worse because it would assume that any Vector{ExceptionState} has the semantics of an exception stack rather than a simple array.

@c42f
Copy link
Member Author

c42f commented Nov 8, 2018

Travis failure on linux32 is failure of a single test, due to #29923

The appveyor failure on 64 bit is really odd (a timeout? but with no error message or other indication of what went wrong?) I've restarted that and we'll see.

@c42f
Copy link
Member Author

c42f commented Nov 8, 2018

Win64 CI problem has symptoms the same as #29603

@StefanKarpinski
Copy link
Sponsor Member

StefanKarpinski commented Nov 8, 2018

Would it make sense to have each exception have next::Union{Nothing,Exception} field instead? That way you can ask for the current exception what, if any, the previous exception was. My reasoning being that it may be less disruptive to just add a field to each exception type since code that isn't prepared to get an exception stack still gets an exception object while code that is prepared can still access the stack easily enough.

@c42f
Copy link
Member Author

c42f commented Nov 8, 2018

It does make sense and in some ways would be more natural and less disruptive. But the problem is user exception types: how do we ensure they actually have such a field? Basically, I don't think we can, at least not without the change being hugely breaking. We even allow people to throw things which aren't <:Exception! I tried to argue that this was a bad idea before but I'm not sure people were convinced (see #21886).

Given we don't have structural inheritance, that leaves us with composition:

struct ExceptionState
    exception
    backtrace
    next::Union{Nothing,ExceptionState}
end

Which I think would be quite a natural way of expressing this. But unfortunately no less breaking and somewhat less flexible to use than just doing what I've done in this PR, and shoving them all in a Vector. Thoughts?

@StefanKarpinski
Copy link
Sponsor Member

Hmm. That's interesting: this may be the first compelling case I've encountered where allowing abstract types to have fields would be really useful—it would allow us to insert a next field into every subtype of Exception.

@c42f
Copy link
Member Author

c42f commented Nov 8, 2018

By the way, my feeling was that there's no harm in providing a simple function-based interface to the exception stack system and seeing how that pans out.

Clearly it's not the end game and you'd like it to integrate better with the syntax. But the best syntax also depends on deeper design questions about how exceptions should be matched.

... having said that I didn't want to talk syntax, now I can't resist suggesting the following which popped into my head:

try
    # blah
catch es...  # equivalent to `es = current_exceptions()`
    # whatever
end

base/exports.jl Outdated Show resolved Hide resolved
base/error.jl Outdated Show resolved Hide resolved
@@ -91,28 +91,34 @@ function catch_backtrace()
return _reformat_bt(bt[], bt2[])
end

struct ExceptionStack <: AbstractArray{Any,1}
Copy link
Sponsor 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.

@c42f c42f requested a review from vtjnash December 4, 2018 01:41
@c42f
Copy link
Member Author

c42f commented Dec 4, 2018

I've rebased, fixed the minor conflict and renamed include_bt.

The remaining question in my mind is what to do about ExceptionStack. It's annoying to have a new type, but there's also reasons to want a type for this so that it can be show()n (and otherwise dispatched on) in contexts which are more general than REPL error reporting.

@codecov-io
Copy link

codecov-io commented Dec 4, 2018

Codecov Report

❗ No coverage uploaded for pull request base (master@7bc3742). Click here to learn what that means.
The diff coverage is n/a.

Impacted file tree graph

@@            Coverage Diff            @@
##             master   #29901   +/-   ##
=========================================
  Coverage          ?   39.06%           
=========================================
  Files             ?      344           
  Lines             ?    61454           
  Branches          ?        0           
=========================================
  Hits              ?    24008           
  Misses            ?    37446           
  Partials          ?        0

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 7bc3742...ff25fee. Read the comment docs.

@c42f
Copy link
Member Author

c42f commented Dec 5, 2018

Win64 issue appears to be #29603 again, restarted.

@JeffBezanson
Copy link
Sponsor Member

My impression is that an exception or exception stack needs to be passed to a context that expects it --- it usually doesn't make sense to have a context that expects an arbitrary value, and then sometimes treats it as an error based on its type.

@c42f
Copy link
Member Author

c42f commented Dec 6, 2018

Yes logging backends can have magic autodetection of "I think this vector looks structurally like it contains exceptions and backtrace pairs" and they can dig exception stacks out of the arbitrary list of user-supplied key=value pairs. I'm just somewhat uncomfortable having to re-infer this information when it was present in the first place.

I'm also not sure why showerror isn't just show with some special IOContext variable set, and this would be much easier to do if we had a special purpose type.

Putting those concerns aside in the interest of getting this done, would it be ok to define a type alias

const ExceptionStack = Array{NamedTuple{(:exception, :backtrace),T}, 1} where {T <: Tuple}

and to remove the special show for ExceptionStack (people can use showerror if they like)? In this case we will also need special handling inside ConsoleLogger so that we can call showerror there rather than show.

@c42f
Copy link
Member Author

c42f commented Dec 6, 2018

@JeffBezanson As food for thought I've added a commit c886fef which removes both ExceptionStack and the specialized show, but keeps a type alias. Because show was removed, I had to add something to ConsoleLogger so that exception stacks can be displayed in log messages in a useful form.

It's not too bad but on the whole it seems like a semantic step backward. On the upside it removes a small amount of code.

@vtjnash
Copy link
Sponsor Member

vtjnash commented Dec 6, 2018

Hm, yeah, that doesn't seem great. I think wrapping the vector in an <:Exception makes sense, since the point is that you want to be able to treat this as a single exception. In that way, it's similar to other existing cases of the same, such as CompositeException, RemoteException, and LoadError. In various cases, that is exactly the point of this new object too. Consider for instance a case like notify(error=true) where we know we're going to lose the backtrace, so we're using current_exceptions to build an explicit representation of the state of all exceptions—and then packaging that up to use as an Exception object itself too.

@vtjnash vtjnash added the status:triage This should be discussed on a triage call label Dec 6, 2018
@JeffBezanson
Copy link
Sponsor Member

Yes logging backends can have magic autodetection of "I think this vector looks structurally like it contains exceptions and backtrace pairs" and they can dig exception stacks out of the arbitrary list of user-supplied key=value pairs. I'm just somewhat uncomfortable having to re-infer this information when it was present in the first place.

That's not at all what I meant to suggest. Looking at the overall type of the value, or the element type, or the types of objects contained in it are all variations of the same thing: examining the value itself to determine how to use it. I'm saying there needs to be some information outside the value itself to tell you what it represents. For example as @vtjnash mentioned notify(cond, val, error=true) treats val as an exception. We don't do that based on the type of val. Similarly a special key could be used in logging e.g. exception=x.

@c42f
Copy link
Member Author

c42f commented Dec 7, 2018

Sorry @JeffBezanson, I just couldn't catch your meaning before. I've reverted that last commit (will be rebased away later).

Similarly a special key could be used in logging e.g. exception=x

Right, this is exactly what the logging documentation already says :-) However, I'm worried about how to show the value of exception stacks to the user, especially when they pass it as a different key... let's say for purely informational purposes:

@info "Ignoring some error" ignored_error=current_exceptions()

If show doesn't do something useful for the type of object returned by current_exceptions() this will be completely unreadable in the ConsoleLogger (or other logger) output. Which is exactly why I want some kind of distinguishable type for this.


it's similar to other existing cases of the same, such as CompositeException, RemoteException, and LoadError

Thanks @vtjnash that's an extremely relevant point. Previously things like LoadError had to eagerly capture the root cause exception to avoid it being lost. Now the exception stack does that automatically for all exceptions so LoadError could just leave the original exception on the stack and this PR means it will be reported to the user. (However we still need RemoteException and CompositeException for out of process exceptions.)

@c42f
Copy link
Member Author

c42f commented Dec 7, 2018

In the spirit of seeing how LoadError/InitError can be made more consistent with the new system, I've changed them from using jl_rethrow_other to jl_throw. With this PR's REPL changes, these would be reported as follows:

2018-12-07-190857_784x460_scrot

The previous exception is still eagerly captured and stored in LoadError. Not sure how that could (or should) be changed without breaking compatibility.

@JeffBezanson
Copy link
Sponsor Member

Yes, I agree LoadError (and InitError) saving the last exception is now redundant; nice! I would say keep everything the same except the printing of LoadError, which now doesn't need to show its inner exception. In 2.0 we can remove those fields.

@vtjnash
Copy link
Sponsor Member

vtjnash commented Dec 7, 2018

Unrelated to this PR, but just from looking at that screenshot, we should eventually hide all frames (but one) that are printed in a earlier (in printing order) list

@JeffBezanson
Copy link
Sponsor Member

Also the simplest thing to do here is just unexport catch_stack for now.

@Keno Keno removed the status:triage This should be discussed on a triage call label Mar 14, 2019
@Keno
Copy link
Member

Keno commented Mar 14, 2019

Triage tag was outdated. If there's something left to triage, re-apply the tag and specify the issue.

@JeffBezanson JeffBezanson modified the milestones: 1.2, 1.3 Apr 25, 2019
@JeffBezanson JeffBezanson modified the milestones: 1.3, 1.4 Aug 15, 2019
Copy link
Sponsor Member

@vtjnash vtjnash left a comment

Choose a reason for hiding this comment

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

LGTM, conditional on addressing the needs-tests tag (and needs a rebase).

@vtjnash
Copy link
Sponsor Member

vtjnash commented Apr 9, 2021

bump. Are you able to rebase?

* 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.
@c42f c42f force-pushed the cjf/show-exception-stacks branch from ff25fee to 35a501b Compare May 14, 2021 03:36
@c42f c42f removed the needs tests Unit tests are required for this change label May 14, 2021
@c42f
Copy link
Member Author

c42f commented May 14, 2021

Wow, this PR has languished for a long time :-(

I've found some time to rebase this and update the tests.

I'll plan to merge it tomorrow, provided CI passes.

@c42f c42f merged commit 9113c01 into master May 15, 2021
@c42f c42f deleted the cjf/show-exception-stacks branch May 15, 2021 03:35
shirodkara pushed a commit to shirodkara/julia that referenced this pull request Jun 9, 2021
* 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.
c42f added a commit to c42f/Compat.jl that referenced this pull request Jun 26, 2021
See JuliaLang/julia#29901

This is limited to julia-1.1 and above because earlier versions don'
have the necessary runtime library support (Base.catch_stack() etc).
johanmon pushed a commit to johanmon/julia that referenced this pull request Jul 5, 2021
* 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.
c42f added a commit to c42f/Compat.jl that referenced this pull request Aug 16, 2021
See JuliaLang/julia#29901

This is limited to julia-1.1 and above because earlier versions don't
have the necessary runtime library support (Base.catch_stack() etc).
martinholters pushed a commit to JuliaLang/Compat.jl that referenced this pull request Aug 19, 2021
* Port Base.current_exceptions() to older Julia versions

See JuliaLang/julia#29901

This is limited to julia-1.1 and above because earlier versions don't
have the necessary runtime library support (Base.catch_stack() etc).

* Also implement current_exceptions on julia-1.0

* Tweak readme to mention Julia-1.0 limitations
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
domain:error handling Handling of exceptions by Julia or the user
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants