Rx.NET v6.1 New Feature: ResetExceptionDispatchState()
Rx.NET
In this video, Ian Griffiths introduces the new ResetExceptionDispatchState
operator in Rx.NET 6.1 released in October 2025.
He explains the peculiar behaviour of exception stack traces that led to the creation of this operator, following feedback from Adam Jones.
The video delves into how exception state is managed in .NET and the specific issues that arise when exceptions are reused without being re-thrown. Ian demonstrates the problem with code examples and shows how the new operator resolves it.
Full documentation is available at Introduction to Rx.NET.
- 00:00 Introduction to Rx.NET's New Feature
- 00:35 Background and Origin of the New Operator
- 02:35 Understanding Exception State in .NET
- 05:37 Demonstrating the Issue with Examples
- 12:31 Introducing the
ResetExceptionDispatchState
Operator - 15:18 Conclusion and Further Resources
Transcript
I'm gonna talk about the new reset exception dispatch state operator that we've added to Rx.NET. So we released Rx version 6.1 in October 2025. And the reason that's a minor bump release is that we've added some new features, so there's DisposeWith
and TakeUntil(CancellationToken)
, and there are separate videos on both of them.
And today I'm gonna talk about the ResetExceptionDispatchState
operator. Now if you've been following Rx's progress, you'll know we're also working on fixing some packaging issues, but that's not gonna come out until Rx v7.0.
What is the reset exception dispatch state operator about? So this originated from some feedback we had from Adam Jones. Adam initially reported a peculiar behavior, and then we discussed it and worked on some ways that we might be able to deal with this and eventually came up with the design for the ResetExceptionDispatchState
, with his review and input. So, thank you very much to Adam!
So, the observed behavior that has led to this addition is that sometimes you can end up with some very odd looking stack traces in exceptions in Rx. So, if you use Rx in a way that an exception that originates from an observable source ends up turning into an actual thrown .NET exception. So, for example, if you await
an IObservable
and that IObservable
produces an error, then sometimes you can end up with strange repetitions in your stack traces.
So, if you look at this stack trace on the screen here, you can see it's basically got the same information three times over. And actually where this was thrown from, the call stack just had that one time over. This does not reflect reality, and this was the behavior that led to the initial bug report.
Now this only happens under certain circumstances, so you'll only see this behavior if you have a single exception object that gets reused without being thrown between those uses. So without there being an actual C# throw
operation or an equivalent in a different language. And not only does that have to happen, but Rx also has to convert that error from the normal Rx OnError
mechanism into an actual rethrow. So, this happens when you await
an observable that ends up calling OnError
on its observer, for example.
And what's actually happening here is that the exception state is not being reset. So what do I mean by exception state? When an exception is thrown, the .NET runtime captures certain contextual information in order to report information about where the error actually came from. So this includes the stack trace, but it also includes certain other information that is known collectively as the crash bucket or the fault bucket. And if, for example, you're using Windows Error Reporting, this enables errors to be distinguished from one another.
So, the basic idea behind this exception state is to be able to distinguish between different causes of exceptions and application developers can sign their applications up to receive information about this if the user consents. And the basic idea here is, so if lots of users of your application are having the same problem, you can find out about that and prioritize fixing that problem over any others.
So the idea is that when an exception is thrown, this fault bucket information is captured and attached to the exception. And if that exception eventually gets thrown back out of the application and causes it to crash completely, that crash bucket information can be recorded by Windows. So that's the exception state that I'm talking about and it includes a stack trace as well.
And by design, Rx does not reset that exception state when you give it an exception, because there might be important information there, you might actually want to record that information. So actually the behavior we're seeing is not technically a bug, and actually you can get exactly the same weird multiple copies of the stack trace; it can happen with .NET without using Rx, this is actually a .NET runtime feature that occurs under certain circumstances, and the reason Rx is not resetting this exception state is that we want to flow exception state correctly in certain other circumstances and fundamentally, there are just some rules you have to follow if you don't wanna run into this behavior, but that was never previously obvious to developers using Rx.
So we've done a couple of things. We've updated the documentation on the https://introtorx.com site to clarify the rules around this, to clarify that the original problem that Adam Jones reported to us is actually expected behavior because the coding question was not conforming to the rules. We've now made it clear what the rules are, so that people can know they should expect this, but also even if you did understand the constraints, it wasn't always that easy to do the right thing, and so we've added this ResetExceptionDispatchState
operator to make it easy to do the right thing.
So let's take a look at it in action. So I'm actually gonna start with an example that shows why it is that Rx doesn't just reset this exception state. So I'm here, I'm using the Observable.Create()
method to build a new IObservable
that actually executes by running some code. So what this does is it's gonna try and open a file, and then it's gonna read each line from the file in turn and then deliver that to whoever has subscribed to this observable. So, this is like an imperative way of writing an observable source.
And then down here I subscribe to that source and I attempt to pull out the first non-empty line from the file. So, the thing is that the file I'm trying to open doesn't actually exist on my system, so this line here is gonna fail. So, what do you think will happen when we try to essentially subscribe to this observable source down here? Let's run it and find out.
So, you can see we get this FileNotFoundException
, and you can see I've actually got a double stack trace. We've got one of these markers here that says, there's a stack trace here, then another stack trace, and this is typical of asynchronous exception handling in .NET. You get the original point at which the exception was thrown, and we can see that's happened on line 5 of my program and that is indeed the line that tried to open the file in the first place. So that was the original point at which the exception got thrown, but then it got thrown again at a different location because we are using await
.
So, line 13 of my program, if I look at that, that's this one here. Here is where it got re-thrown. So, it originally got thrown here. Rx actually caught it and then when we did the await
Rx re-threw it and we get the full stack trace information and as it happens, the crash bucket's also gonna identify this line here, all that information is present because Rx didn't reset it. And this is by design. We don't want to throw away this information.
So this example shows exactly why we are not discarding that information by default. But now I'm gonna show you a program where that causes a problem. Let me switch the debugger over to that one.
So here I'm using the throw
method to build an observable source that will instantly just report this exception to anyone who subscribes to it. So inside the subscribe, which is gonna call your observer's OnError
, straight back passing in this exception. But then I've written this loop here that awaits
that observable each time round the loop and then prints out the exception.
Let's run this and see what happens. So, the first time round the loop, it catches the exception and reports the stack trace and we can see it's come from inside some of Reactive's implementation, but we can see it's being reported as originating as line 12 of my code. Let's take a look at that. Line 12 is this one here, it is the await
. So far so good.
But now I'm gonna let this run again and I just need to clear that selection. Okay, so now we've hit the exception handler again, but this time we've got two copies of the stack and that does not accurately reflect what happened. The stack's not getting any deeper here. This is just a reporting error. The exception stack trace has grown, and if I go around again this time, it's gonna be three high. And if I let this run all the way to the end, 10 times round the loop, then at the very last time, we got 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 copies of the stack trace.
So this is the problem that was originally reported to us, but as I said, it's not technically a bug. For one thing, this is nothing to do with Rx. We could actually take Rx completely out of the equation and just construct the exception directly. And then if instead of awaiting my now doesn't exist observable source, if I do an await
of Task.FromException
of that exception, so now I'm just using .NET's Task
class and telling it to wrap this exception as a faulted task.
If I run this again and let's just breakpoint the same place. So the first time around we get just a single line exception. But if I run it around again, now we get two copies of the stack trace and we run it again. Now we get three copies and so on.
So this problem isn't unique to Rx. It happens anytime you cause an exception to be re-thrown, either through await
or through some other mechanism that's able to re-throw a previously captured exception.
If you re-throw an exception that was never technically thrown in the first place, and here you can see I've constructed my exception, but there's no throw
operation anywhere in sight. If you do that, then each time you re-throw the exception, it just appends a bit more information to the stack trace. Because .NET's not really expecting you to do this.
The assumption when you re-throw is that it thinks you already have an exception that was thrown and you want to append a bit more information to the exception state saying, okay, this will have the original stack trace in it, but now you are doing an await
, so I am going to add in the stack trace for where you are calling this from, appending it to what was already there and because nothing ever resets this exception, it just goes round and round again, and just keeps getting longer and longer.
And so if I put this back how it was, go back to the Rx version, that is why this is happening. We're constructing a single exception object and we're repeatedly causing Rx to re-throw it, but there's no original throw
event. You might think, shouldn't the throw
operator do that for us? Shouldn't this reset the thing? Shouldn't this do what throw
does in C#? The reason it doesn't is that Rx says, you might have attached important information in here, and there's no good way to discover when you've passed in an exception that doesn't have exception state attached, there isn't an efficient way for us to discover that. So we just have to assume it might have that state, and therefore we don't touch it.
So how do we fix this? The first thing to understand is what the rules are. So the first thing we've actually done is we have modified the documentation. So this is a page from the Intro to Rx book, which is available online on the https://introtorx.com website. It's also available in PDF form and the chapter about "Leaving Rx's World". So the chapter that talks about things like coming out of the world of IObservable
and into the world of Task
, which is what we are doing if we await
an observable, we've added a section at the start here that talks about exception state and makes very clear what the rules are.
It says that if you are using a mechanism that takes exceptions delivered from an observable and that re-throws them, then you as the developer are responsible to ensure that either you don't reuse those exception objects. So each exception object is used just once or you do something to reset the exception dispatch state.
So this is the first thing we've done. We've documented these rules. Clearly these were never explicitly documented before, so it's not surprising people don't know that this isn't technically allowed. And then the other thing we've done is we've said, if you are in this situation where you do want to throw the same exception multiple times, we'll give you this new operator called ResetExceptionDispatchState
.
So this operator passes all of the events that it receives straight through. So it doesn't change anything except for one thing: if the upstream source of the throw
, in this case, if the upstream observable happens to produce an exception, if it calls OnError
, then before this passes that on, it actually resets the ExceptionDispatchState
, it does something equivalent to throwing the exception in order to reset the stack trace and all the other exception states.
So if we run this version, the first time around we see the stack trace pointing at line 13 of main, and also you'd get this inner one saying, okay, there's actually, the exception state was reset inside the ResetExceptionDispatchState
. That's a side effect of us resetting the state inside that operator. So you'll always see this, but if we run it again, if I go this around the loop a second time, throwing the same exception, this time we just see one copy of the same stack trace. We don't see the thing growing. So if I let this run all 10 times, even the 10th time, you just get a single copy of the stack trace.
So to sum up, the basic problem here is that when you re-throw an exception in .NET without actually doing a real throw
, you end up with these slightly strange results. The exception state accumulates, and that can cause odd looking stack traces. That's a basic .NET runtime behavior. We can't change this, and this is actually the expected behavior.
So what we've done is we've changed the documentation to make it clear that this is the situation. So people now have some way of knowing that they weren't supposed to do that in the first place. But we've also added this new operator ResetExceptionDispatchState
, which you can add in to say, I don't want to keep the exception state. In fact, I need to not preserve the exception state. I need the exception state to be reset every single time an exception flows through this point in my subscription. And if you put that in, it will reset the exception each time the source produces it, avoiding the problem.
And if you go to https://introtorx.com, you can see much more detail about exactly how this works.
My name's Ian Griffiths. Thanks very much for listening.