core-libraries-committee
core-libraries-committee copied to clipboard
Exception backtrace proposal: Part 4: Rethrowing
Tracking ticket: #164
This proposal attempts to summarise the interface design of the exception annotation scheme proposed in GHC Proposal #330. Specifically, this proposal covers the changes described in section 2.6.
Note that the GHC Proposal is free-standing; no reading of the discussion which lead to its current accepted state should be necessary to understand its contents. Consequently, to avoid repetition I will refer back to the GHC Proposal instead of repeating myself here. I will, however, attempt to give some color to the interfaces by providing typical usage examples where necessary. However, the GHC Proposal is to be considered the canonical definition of the interfaces; in particular, section 2 and its subsections precisely captures the changes proposed in base.
Preserving context across rethrowing
A common pattern in Haskell applications is to catch an exception and later rethrow it (or a value derived from it) in the handler. However, this pattern defeats our annotation mechanism as context of the original exception will not be propagated to the rethrown exception. In this proposal we address this weakness by teaching catch and similar operations to augment exceptions thrown from handlers with the handled exception (and its context).
Specifically, we propose to introduce a new ExceptionAnnotation (section 2.6):
newtype WhileHandling = WhileHandling SomeException
instance ExceptionAnnotation WhileHandling
We then modify catch to annotate exceptions thrown from the handler with a WhileHandling containing the handled exception:
catch :: Exception e => IO a -> (e -> IO a) -> IO a
catch (IO io) handler = IO $ catch# io handler'
where
handler' e =
case fromException e of
Just e' -> unIO (annotateIO (WhileHandling e) (handler e'))
Nothing -> raiseIO# e
We then introduce a catchNoAnnotation exposing the old semantics of catch for cases where this is desired.
Migration
This change has no effect on program semantics modulo ExceptionContext. However, it will cause a slight increase in allocations when catch'ing exceptions.
WhileHandling is going to make it way harder to get a complete picture of the annotations present on an exception. The design of annotated-exception stores a [Annotation], and then I do some hacky thing where I merge CallStacks together. The result is that you get all the annotations, easy peasy - answering the question "What annotations are present on this exception?" is trivial, you look at the list.
With this, we now have an indirection - we need to dig into the ExceptionAnnotation context, then look for a WhileHandling, dig into the SomeException, and now we have the second level of annotations.
getAllAnnotations :: SomeException -> [SomeExceptionAnnotation]
getAllAnnotations se =
let this = getAllExceptionAnnotation $ someExceptionContext se
causedBys = mapMaybe (\ (SomeExceptionAnnotation ann) -> cast ann :: Maybe WhileHandling) this
otherContexts = concatMap ( \(WhileHandling someException) -> someExceptionContext someException) causeBys
in this <> otherContexts
I'm not convinced this holds up if there are other exception wrapper types, or if exception hierarchies are used. I anticipate that this will lose annotations if you don't know, up front, all of the casts that you need to do to dig around and find the annotations.
@parsonsmatt, I'm a bit unclear on what concretely you are concerned about. Are you worried that there might be exceptions (and therefore ExceptionContexts) buried in annotations other than WhileHandling? If so, it seems that this problem is far more general than WhileHandling; rather this is inherent in the dynamically-typed nature of annotations.
The design of annotated-exception right now has the annotations all in a flat list. There's literally no logic for finding the annotatations - they're all present, all the time, very easily.
The only occasion where we actually do have hidden annotations is when we've got a "catch all" exception wrapper and the developer expliciltly hides annotations. As an example, we have this in our codebase:
data ClientError
= InvariantViolated Text
| {- other cases -}
| DatabaseError SomeException
The way this DatabaseError is used is something like this:
clientDB :: DB a -> AppM a
clientDB action =
runDB action `catches`
[ Handler \(e :: ClientError) -> throwWithCallStack e
, Handler \(other :: SomeException) -> throwWithCallStack (DatabaseError other)
]
This is "safe" because catches in annotated-exception inspects the type of these, and if you provide a handler that's not an AnnotatedException, it'll inject another handler that's on AnnotatedException e, and reattach the exception context - so the above really expands to:
clientDB :: DB a -> AppM a
clientDB action =
runDB action `catches`
[ Handler \(AnnotatedException anns (e :: ClientError) ->
checkpointMany anns $ throwWithCallStack e
, Handler \(e :: ClientError) -> throwWithCallStack e
, Handler \(AnnotatedException anns (other :: SomeException) ->
checkpointMany anns $ throwWithCallstack other
, Handler \(other :: SomeException) -> throwWithCallStack (DatabaseError other)
]
You can defeat this safeguarding by manually catching AnnotatedException and then manually stuffing it into a SomeException.
If annotated-exception weren't careful to re-attach the context when throwing, then we'd have a top-level thing like:
AnnotatedException
{ annotations =
[ Annotation @CallStack ...
]
, exception =
DatabaseError
(SomeException
(AnnotatedException
{ annotations =
[ Annotation @CallStack ...
, Annotation @SomethingElse ...
, {- more hidden annotations -}
]
, exception =
TheActualProblem
}
)
)
}
Now, we can't just fold over the [Annotation] on the top-level. We have to fold over the annotations, then dig into the exception type if it is a wrapper. If it is a wrapper, (or contains a wrapper somewhere), then we need to unpack any possible annotations from there.
So the design of annotated-exception tries, as much as possible, to ensure that there's only one level of annotations, and that's the very top level. Naturally we can't stop the user from doing something that defeats that entirely, but the result of this is that we almost never lose or drop annotations.
This ends up being a really nice property in our app, because we just catch the final AnnotatedException, fold over the [Annotation] to modify our error report, and away we go. There's no need to dig through all the various dynamic type wrappers, and we don't have to recursively dig through other annotations to find them.
The design proposed here will double the inconvenience of this sort of digging by forcing you to dig through not just any possible exception types, but also the CausedBy wrapper, which itself has a SomeException, which may have it's own CausedBy annotations in addition to it's own wrapper exception.
To be a bit more concise:
- There should be a single function to get all of the annotations for an exception.
- As much as possible, the annotation design should preserve all annotation information, such that it is easy to recover it from the top-level exception
annotated-exceptionhas special behavior forCallStack- when merging two[Annotation], we dig out theCallstackannotation if present and merge them together. This provides us with a single completeCallStack. I can see an argument that this sort of special-handling isn't ideal and instead the consumer should be responsible for it - ie, a special functionannotationsCallStack :: [Annotation] -> Maybe CallStackthat finds allCallStacks and merges them in the appropriate manner.
OK, that was still too verbose. Let me try and distill further:
- If the annotations of rethrown exceptions are hidden in a
WhileHandlingannotation, then they are less visible and less useful. This is worse than if they are always present on the top-levelSomeExceptionfor every use case I can imagine.
@parsonsmatt perhaps it alleviate your concern if we introduced another method to Exception:
class Exception a where
nestedExceptions :: a -> [SomeException]
nestedExceptions _ = []
This would allow us to preserve the clear semantics of annotations while also enabling reconstruction of all relevant annotations from an exception.
That does help, yes. I still think it's valuable to provide a function getAllAnnotations :: SomeException -> [SomeAnnotation] that doesn't require end users doing anything other than writing their instancs correctly. So that may mean we need a similar method on IsAnnotation
@bgamari @parsonsmatt did you converge to a final proposal which we can vote on?
I just wanted to reiterate that this is a very well-established approach taken by Java, which I think provides evidence that the inconvenience is not so great.
Additionally, merging annotations seems hard in general. I find this concerning:
this provides us with a single complete CallStack
I don't see how this can be correct? The exception thrown from the handler and the exception that was originally caught may have wildly different call stacks. If we want to give people the full picture of what happened I think we need to present those separately.
Does nestedExceptions even need to be a class method? Isn't it enough to just go through the WhileHandlings?
Does
nestedExceptionseven need to be a class method? Isn't it enough to just go through theWhileHandlings?
I think @parsonsmatt's concern applies to a fairly broad spectrum of cases. For instance, consider Network.HTTP.Client.HttpException, which may hide exceptions (and possibly their annotations).
Ah yes, indeed, we would expect other mechanism similar to catch to use other mechanisms for storing exception "causes". I do think the Java approach there is unsatisfying, in that they have Throwable getCause() on exceptions, but this makes a somewhat unjustified assumption that there is a single such exception and that it is a "cause".
At the moment I am leaning towards addressing @parsonsmatt's concern with the following additions:
class ExceptionAnnotation a where
...
-- | Extract any exceptions nested within an annotation.
exceptionAnnotationNestedExceptions :: a -> [SomeException]
class Exception a where
...
-- | Extract any exceptions nested within an exception.
nestedExceptions :: a -> [SomeException]
flattenAnnotations :: Exception a -> [SomeExceptionAnnotation]
flattenAnnotations e = concat
[ exceptionAnnotations e
, concatMap flattenAnnotations (concatMap (\(SomeExceptionAnnotation ann) -> exceptionAnnotationNestedExceptions ann) (exceptionAnnotations e))
, concatMap flattenAnnotations (nestedExceptions e)
]
However, there is a considerable design space and these additions seem a bit orthogonal to the simple interface described in in this proposal. Consequently, I will open a new proposal for this.
Consequently, I would like to submit this to the CLC for a vote.
I have opened https://github.com/haskell/core-libraries-committee/issues/250 with the above proposal.
Additionally, merging annotations seems hard in general. I find this concerning:
this provides us with a single complete CallStack
I don't see how this can be correct? The exception thrown from the handler and the exception that was originally caught may have wildly different call stacks. If we want to give people the full picture of what happened I think we need to present those separately.
The callstacks would have some divergence, but the original exception's callstack is likely more important.
foo =
bar `catch` conditionallyRethrow
The callstack up to foo will be the same. Then we go down the bar callstack, throw an exception, and hit catch. Then conditionallyRethrow starts it's own callstack, which (in my experience) is almost always one or maybe two frames.
foo =
bar `catch` \(e :: IOException ) ->
if quux e then pure 0 else throwIO FooException
In this case, having the callstacks merged is vastly better than having a separate CallStack for FooException, which gets rendered, and forgets everything that happened in bar (unless you reconstruct it by digging through the other annotations and WhileHandling etc), and then if you do reconstruct it, you have to repeat the calls up to foo anyway!
The machinery in #250 would allow a libray author to write such a function, providing that all of the exception wrapper types have been updated and properly written. With a flat exception annotation list, we start with everything we need. With a nested structure, we start with very little, and need to do more work through the ecosystem to bring things into compliance, and we need to tell folks to write their own methods of nestedExceptions et al.
So I think there's significant extra work imposed on the whole ecosystem with #250, and concordant risk that exceptions won't implement these methods, and we'll drop information on the floor and make it harder to recover.
If we accumulate exception annotations on the top level, then we get deep annotations and callstacks immediately when the feature releases, for all library versions. If we do the approach in #250 then we don't get this information usefully until we release the GHC with this, then we need to update all libraries that have exception wrappers (with CPP so it's backwards compatible), and then we only get nice information when every library in the chain has been updated.
For conciseness:
- +1 on keeping all annotations on a top-level
SomeException - +1 on also having a
WhileHandlingannotation - -1 on anything that loses annotations on the top-level
SomeException - +1 on #250 (which is indeed orthogonal to this proposal)
In this case, having the callstacks merged is vastly better than having a separate CallStack for FooException, which gets rendered, and forgets everything that happened in bar (unless you reconstruct it by digging through the other annotations and WhileHandling etc), and then if you do reconstruct it, you have to repeat the calls up to foo anyway!
You can just... render it? I don't see why we wouldn't get good rendering by default. e.g. consider the following Java stack trace I found on the internet:
StudentException: Error finding students
at StudentManager.findStudents(StudentManager.java:13)
at StudentProgram.main(StudentProgram.java:9)
Caused by: DAOException: Error querying students from database
at StudentDAO.list(StudentDAO.java:11)
at StudentManager.findStudents(StudentManager.java:11)
...
Caused by: java.sql.SQLException: Syntax Error
at DatabaseUtils.executeQuery(DatabaseUtils.java:5)
at StudentDAO.list(StudentDAO.java:8)
...
This has all the information, without showing anything misleading. I do think that a requirement should be that we get default rendering at least as good as this for the WhileHandling annotation. If what we're actually going to get is just the first stack trace then indeed that's no good
Is your objection that nestedAnnotations is too generic, and so we don't have a single notion of "cause" like Java exceptions do which we can lean on to get good rendering?
@bgamari @parsonsmatt @michaelpj please signal when you converge to a consensus on this.
Is your objection that
nestedAnnotationsis too generic, and so we don't have a single notion of "cause" like Java exceptions do which we can lean on to get good rendering?
@parsonsmatt it would be great to have a response to @michaelpj's question. It's hard to know how to amend the proposal without a clear picture of what use-case we are designing for.
Ultimately, I suspect that at this point this final part of the exception backtrace series will likely be deferred to GHC 9.12; it's simply too late in the release cycle to comfortably come up with a suitable design and implement. It is nice that the proposals are now modular enough to allow this sort of deferral.
@parsonsmatt just a gentle reminder that it would be nice to find a conclusion here.
Is your objection that nestedAnnotations is too generic, and so we don't have a single notion of "cause" like Java exceptions do which we can lean on to get good rendering?
No, the objection is that it requires all authors of annotations and exceptions to properly implement a method, where the obvious/easy default is to drop the information on the floor.
My concern is with losing information, or hiding information, or just generally making it much harder to find and use the information in a useful way. Keeping the annotations in a top-level property and not nesting them accomplishes this very nicely. Doing anything else is guaranteed to lose information and make things harder.
nestedException _ = [] is particularly troublesome. It means that you need to opt-in to providing this behavior, which can only be done on certain library versions (when the library author actually releases a new version, along with the necessary CPP to keep it backwards compatible).
If we just preserve annotations in the top-level SomeException, then all users immediately get a full picture on day 1 of this releasing. If we require all libraries to upgrade, and then wait to use all the new libraries, then we're going to have a significantly longer and more error-prone adoption of this.
As a case study, we use annotated-exception which does what I suggest. There are no issues caused by having a single top-level list of annotations, and the library tries really hard to preserve that guarantee. The main problem is that the design can't prevent more information to be lost - people are used to these two definitions being equivalent:
handle0 =
action `catch` handler
handle1 =
eresult <- try action
case result of
Left err -> handler err
Right a -> pure a
But with annotated-exception, you actually lose the annotations when you try, unless you specifically try @(AnnotatedException e) instead of try @e. This has been a problem, where using the wrong exception handling strategy results in lost annotations which makes it harder to diagnose/debug/understand runtime errors.
So all of the experience we've had with annotated-exception in our codebase has been positive, with the exception of sometimes losing annotations due to programmer error. This proposal adds significant surface area for programmer error to lose information.
@parsonsmatt I'm struggling to understand your position, unfortunately. You make good arguments against #250, namely that adding a nestedException member to the Exception class with a default implementation basically guarantees it is "liable to be incorrect for existing exception types" (as @bgamari noted but perhaps could have emphasized more). But I'm not sure exactly what you are arguing for instead.
One could imagine changing catch to copy/merge the annotations from the rethrown exception, as well as adding a WhileHandling annotation, but it seems like that discards information (because you can no longer easily distinguish the original call stack from the call stack at the rethrow site) and I don't see how you can get the correct output from displayException (how do you avoid rendering the original call stack twice, unless you get rid of WhileHandling altogether?).
The original proposal allows displayException to simply do the right thing (along the lines of https://github.com/haskell/core-libraries-committee/issues/202#issuecomment-1930365274). I can see that it could be difficult to write some kind of alternative rendering of exceptions that works for arbitrary wrapper types, but do you have a particular use case of that nature in mind?
In annotated-exception, I have addCallStackToAnnotations, which calls mergeCallStack, which combines two CallStacks and removes duplicates.
catch delegates to catches, which delegates to checkpointMany, which adds the annotations back onto the thrown exception, including calling addCallStackToException with the callStack from HasCallStack.
In this design, I believe we'd just grab the annotations from the SomeException we are catching and stuff them as annotations into any rethrown exceptions.
catch :: Exception e => IO a -> (e -> IO a) -> IO a
catch (IO io) handler = IO $ catch# io handler'
where
handler' e =
case fromException e of
Just e' -> unIO (annotateManyIO (exceptionAnnotations e) (handler e'))
Nothing -> raiseIO# e
I think I can distill this down to a few questions, most of which are orthogonal:
- Do we include a
WhileHandlingannotation incatch? a. If so, do we want to do anything to avoid duplication? iecatch action throwIOwould include aWhileHandling, even though the exception is the same. - Do we combine annotations in
catch? a. If not, we almost certainly want to use aWhileHandlingannotation to cover that - Do we merge callstacks? Or do we keep them separate? a. If we keep them separate, do we want to deduplicate them in some way?
- Should
catchalso annotate with the current callstack? ShouldannotateIO?
My answers:
- Sure,
WhileHandlingseems useful. I wonder if it makes sense to worry about duplication - but then, we don't haveEqfor exceptions generally, so we'd either need to use theshowrepresentation or the type, and both of those aren't ideal. Duplicating an exception into theWhileHandlingmay lead to extraneous information for simple rethrows, and might blow up memory usage if we are doingcatchin a loop. - I think we should combine annotations - whenever we
catch, the handler should re-attach any annotations to whatever exception is thrown. Otherwise, we risk losing valuable nformation. - We've seen a lot of benefit from keeping the
CallStackmerged and deduplicated withannotated-exception. It's relatively straightforward to read and interpret them. - Yes and yes.
catchis in a great position to enrich exceptions with location information.annotateIOsimilarly - who wouldn't want callsite information on that?
We've seen a lot of benefit from keeping the CallStack merged and deduplicated with annotated-exception. It's relatively straightforward to read and interpret them.
Do you have an example of this? I'm really struggling to see how a merged call stacks can be anything other than incorrect, versus the Java approach of keeping the two call stacks from the two throws clearly separate and labelled.
Folks, what's the conclusion on this? Shall we wait until GHC 9.10 is out and battle-tested for a while before continuing the discussion? If that's the sentiment, I'll close this as dormant to reopen once there is more user experience.
We can also just vote as is. Consensus is not a requirement, however it would be a pity to have the proposal defeated because of a minor disagreement. I cannot quite read the room at the moment.
CC @parsonsmatt @bgamari
Folks, what's the conclusion on this? Shall we wait until GHC 9.10 is out and battle-tested for a while before continuing the discussion? If that's the sentiment, I'll close this as dormant to reopen once there is more user experience.
Yes, this is roughly where I fall. I think it's hard to say at the moment what the right course of action is here. I hope to have a blog post out shortly after the release describing the exception backtrace work, which might spur more discussion.
Thanks, @bgamari, let me make it as dormant then. Feel free to reopen later on.
If I understand the discussion correctly, the question is essentially whether we want a hierarchical structure (WhileHandling) versus a flat structure (in the style of annotated-exception). I can see the benefits of the flat structure:
- In some pathological scenarios the hierarchical structure might be very large (it reminds me of the horrors of running real code with
+RTS -xc). - In the case where the same exception is caught and then rethrown we could get quite a bit of duplication. For example, we probably want to be careful in the definition of
bracketand friends, and avoid adding unnecessaryWhileHandlingannotations there. (This is not a new observation, however:onExceptionis already redefined in terms ofNoBacktrace-- althoughNoBacktraceis not exported inbase, unclear why. An oversight?) - In cases where we want to process the annotations in code (as opposed to have a human looking at the result of
displayException), or perhaps extract a specific type of annotations (a valid use case,ExceptionAnnotationhasTypeableas a superclass constraint for a reason) then that might be difficult unless we have some way of getting nested exceptions (which comes with its own difficulties, as discussed above).
I am nonetheless in favour of the original proposal: it feels like flattening loses information, and in difficult debugging scenarios, I might want to trace the exact path of how an exception was raised, including all the individual callstacks of each handler: I share @michaelpj 's concern that merging CallStacks is not in general a very meaningful combination.
Moreover, I think the flattened form can easily be obtained from the hierarchical one, simply by walking the WhileHandling tree, as Michael points out. There was some discussion on this specific point above, which I will come back to in a separate comment.
We are planning to talk about the new exception infrastructure (annotations and backtraces) in the upcoming episode of the Haskell Unfolder next Wednesday (July 17th, 18:30 UTC, live-streamed on YouTube). I will mention the the problem of preserving annotations, and point to this ticket; perhaps this will generate some more interest and discussion.
On the topic of flattening the hierarchical structure: the claim is that we can simply flatten the hierarchy by walking over WhileHandling. I think this claim is correct. Consider a setup like this (where NestException is meant to be a canonical example of the kind of thing that HttpException does, as in @bgamari 's example):
data OriginalException = OriginalException
deriving stock (Show, Generic)
deriving anyclass (Exception, PrettyVal)
data NestSomeException = NestSomeException SomeException
deriving stock (Show, Generic)
deriving anyclass (Exception, PrettyVal)
data Annotation = Annotation String
deriving stock (Show, Generic)
deriving anyclass (ExceptionAnnotation, PrettyVal)
throwOriginal :: IO ()
throwOriginal =
annotateIO (Annotation "hi") $
throwIO OriginalException
rethrowNested1 :: IO ()
rethrowNested1 =
throwOriginal `catch202` \e@OriginalException ->
throwIO $ NestSomeException (toException e)
rethrowNested2 :: IO ()
rethrowNested2 =
throwOriginal `catchFlat` \e@OriginalException ->
throwIO $ NestSomeException (toException e)
where catch202 is catch as proposed originally in this ticket, and catchFlat is an alternative definition that merges contexts; perhaps something like this:
catchFlat :: forall e a. Exception e => IO a -> (e -> IO a) -> IO a
catchFlat io handler = catch io handler'
where
handler' :: SomeException-> IO a
handler' e =
case fromException e of
Just e' -> catch (handler e') (handleRethrows (someExceptionContext e))
Nothing -> throwIO $ NoBacktrace e
handleRethrows :: ExceptionContext -> SomeException -> IO a
handleRethrows origCtxt e@(SomeException e') =
throwIO $ NoBacktrace $
ExceptionWithContext (origCtxt <> someExceptionContext e) e'
Then with the hierarchical approach, we get something like
SomeException
{ someExceptionContext =
ExceptionContext
[ WhileHandling
SomeException
{ someExceptionContext =
ExceptionContext
[ Annotation hi
, <backtrace to throwIO in throwOriginal>
]
, fromException = OriginalException
}
, <backtrace to throwIO in rethrowNested1>
]
, fromException =
NestSomeException
SomeException
{ someExceptionContext = ExceptionContext []
, fromException = OriginalException
}
}
whereas with the flat approach we get something like
SomeException
{ someExceptionContext =
ExceptionContext
[ Annotation hi
, <backtrace to throwIO in throwOriginal>
, <backtrace to throwIO in rethrowNested2>
]
, fromException =
NestSomeException
SomeException
{ someExceptionContext = ExceptionContext []
, fromException = OriginalException
}
}
It is of course true, as @bgamari points out, that this does not solve the general problem of other ways of nesting exceptions. For example, if some library bypasses catch altogether in favour or one that does not manipulate the context at all
rethrowNested3 :: IO ()
rethrowNested3 =
throwOriginal `catchExceptionNoAnnotation` \e ->
throwIO $ NestSomeException e
then we might end up with something like
SomeException
{ someExceptionContext =
ExceptionContext
[ <backtrace to throwIO in rethrowNested3>
]
, fromException =
NestSomeException
SomeException
{ someExceptionContext =
ExceptionContext
[ Annotation hi
, <backtrace to throwIO in throwOriginal>
]
, fromException = OriginalException
}
}
for which it is indeed harder to extract all (nested) annotations; but this is true for both the hierarchical and the flat approach.
So I think the problem of getting nested exceptions should be considered to be an orthogonal concern, independent from the WhileHandling proposal.
(The above experiment is available in the GitHub repo for the Haskell Unfolder, episode 29).
@edsko There appear to be two definitions of onException.
The one in GHC.Internal.IO is defined as:
334 onException :: IO a -> IO b -> IO a
335 onException io what = io `catchException` \e -> do
336 _ <- what
337 throwIO $ NoBacktrace (e :: SomeException)
but in GHC.Internal.Control.Exception.Base as:
191 -- | Like 'finally', but only performs the final action if there was an
192 -- exception raised by the computation.
193 onException :: IO a -> IO b -> IO a
194 onException io what = io `catch` \e -> do _ <- what
195 throwIO (e :: SomeException)
So if you use bracket from Control.Exception.Base then you get different behaviour than if you use it from GHC.IO.
@Bodigrim @haskell/core-language-committee
I think the backtrace mechanism is severely hindered by the lack of a rethrowing mechanism, please can we make progress with this proposal?
At the moment if you have any kind of error handling in your application which uses bracket, try or onException then
you will routinely lose any annotations (unless you include the location explicitly in the error)
For example:
module Main where
import System.Directory
import Control.Exception
import System.OsPath
import GHC.Internal.Foreign.C.String.Encoding
import GHC.Internal.IO.Encoding
data E = E deriving Show
instance Exception E
f1 :: FilePath
f1 = throw E
main = do
f <- getForeignEncoding
let y1 :: FilePath
y1 = throw E
-- Prints a call stack
--print y1
-- Doesn't print a call stack, annotation is discarded
print =<< getModificationTime f1
getModificationTime is a function from directory, which eventually calls withCString which is implemented in terms of bracket which is implemented in terms of onException, which rethrows exceptions losing the annotation.
It is of principle importance that base is modified as all exception handling utilities must propagate exceptions
Are we all in agreement that rethrowing is important for base functions? If we are then please can the disagreements be mediated and a fix to the base libraries prioritised?
For me personally, I prefer the extra structure which WhileHandling approach takes, which seems to be the consensus on this issue. Merging call stacks seems to be incorrect behaviour to me.
In any case, if there is a counter-proposal it should also be implemented and put a CLC vote as this is an important issue to fix! However, if there isn't a counter-proposal which is implemented then I think the CLC should vote on the WhileHandling approach as described in this issue and already implemented in the original patch.
@mpickering I think the CLC is waiting on this: https://github.com/haskell/core-libraries-committee/issues/202#issuecomment-2098815766