From 2c9b8122ea826b643530f54f1a4422da92d3969c Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sun, 11 Oct 2026 23:52:39 +1100 Subject: [PATCH] Preserve SetCurrentStackTrace across throw --- src/Polyfill/ExceptionDispatchInfoPolyfill.cs | 15 ++++++ .../net461/ExceptionDispatchInfoPolyfill.cs | 11 +++++ .../net462/ExceptionDispatchInfoPolyfill.cs | 11 +++++ .../net47/ExceptionDispatchInfoPolyfill.cs | 11 +++++ .../net471/ExceptionDispatchInfoPolyfill.cs | 11 +++++ .../net472/ExceptionDispatchInfoPolyfill.cs | 11 +++++ .../net48/ExceptionDispatchInfoPolyfill.cs | 11 +++++ .../net481/ExceptionDispatchInfoPolyfill.cs | 11 +++++ .../ExceptionDispatchInfoPolyfill.cs | 11 +++++ .../ExceptionDispatchInfoPolyfill.cs | 11 +++++ .../ExceptionDispatchInfoPolyfill.cs | 11 +++++ .../ExceptionDispatchInfoPolyfill.cs | 11 +++++ .../ExceptionDispatchInfoPolyfill.cs | 11 +++++ .../ExceptionDispatchInfoPolyfill.cs | 11 +++++ .../ExceptionDispatchInfoPolyfill.cs | 11 +++++ .../uap10.0/ExceptionDispatchInfoPolyfill.cs | 11 +++++ .../PolyfillTests_ExceptionDispatchInfo.cs | 47 +++++++++++++++++++ 17 files changed, 227 insertions(+) diff --git a/src/Polyfill/ExceptionDispatchInfoPolyfill.cs b/src/Polyfill/ExceptionDispatchInfoPolyfill.cs index b52e8b98..0ba36f85 100644 --- a/src/Polyfill/ExceptionDispatchInfoPolyfill.cs +++ b/src/Polyfill/ExceptionDispatchInfoPolyfill.cs @@ -23,6 +23,21 @@ public static Exception SetCurrentStackTrace(Exception source) { if (source == null) throw new ArgumentNullException(nameof(source)); var stackTrace = new StackTrace(1, true).ToString(); + + // Store it as the "remote" stack trace, which the runtime keeps and prepends to Exception.StackTrace. + // _stackTraceString would be cleared as soon as the exception is thrown. + var remoteField = typeof(Exception).GetField("_remoteStackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); + if (remoteField != null) + { + var remoteStackTrace = + stackTrace.TrimEnd('\r', '\n') + + Environment.NewLine + + "--- End of stack trace from previous location where exception was thrown ---" + + Environment.NewLine; + remoteField.SetValue(source, remoteStackTrace); + return source; + } + var field = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); field?.SetValue(source, stackTrace); return source; diff --git a/src/Split/net461/ExceptionDispatchInfoPolyfill.cs b/src/Split/net461/ExceptionDispatchInfoPolyfill.cs index eb978447..62dc9653 100644 --- a/src/Split/net461/ExceptionDispatchInfoPolyfill.cs +++ b/src/Split/net461/ExceptionDispatchInfoPolyfill.cs @@ -19,6 +19,17 @@ public static Exception SetCurrentStackTrace(Exception source) { if (source == null) throw new ArgumentNullException(nameof(source)); var stackTrace = new StackTrace(1, true).ToString(); + var remoteField = typeof(Exception).GetField("_remoteStackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); + if (remoteField != null) + { + var remoteStackTrace = + stackTrace.TrimEnd('\r', '\n') + + Environment.NewLine + + "--- End of stack trace from previous location where exception was thrown ---" + + Environment.NewLine; + remoteField.SetValue(source, remoteStackTrace); + return source; + } var field = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); field?.SetValue(source, stackTrace); return source; diff --git a/src/Split/net462/ExceptionDispatchInfoPolyfill.cs b/src/Split/net462/ExceptionDispatchInfoPolyfill.cs index eb978447..62dc9653 100644 --- a/src/Split/net462/ExceptionDispatchInfoPolyfill.cs +++ b/src/Split/net462/ExceptionDispatchInfoPolyfill.cs @@ -19,6 +19,17 @@ public static Exception SetCurrentStackTrace(Exception source) { if (source == null) throw new ArgumentNullException(nameof(source)); var stackTrace = new StackTrace(1, true).ToString(); + var remoteField = typeof(Exception).GetField("_remoteStackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); + if (remoteField != null) + { + var remoteStackTrace = + stackTrace.TrimEnd('\r', '\n') + + Environment.NewLine + + "--- End of stack trace from previous location where exception was thrown ---" + + Environment.NewLine; + remoteField.SetValue(source, remoteStackTrace); + return source; + } var field = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); field?.SetValue(source, stackTrace); return source; diff --git a/src/Split/net47/ExceptionDispatchInfoPolyfill.cs b/src/Split/net47/ExceptionDispatchInfoPolyfill.cs index eb978447..62dc9653 100644 --- a/src/Split/net47/ExceptionDispatchInfoPolyfill.cs +++ b/src/Split/net47/ExceptionDispatchInfoPolyfill.cs @@ -19,6 +19,17 @@ public static Exception SetCurrentStackTrace(Exception source) { if (source == null) throw new ArgumentNullException(nameof(source)); var stackTrace = new StackTrace(1, true).ToString(); + var remoteField = typeof(Exception).GetField("_remoteStackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); + if (remoteField != null) + { + var remoteStackTrace = + stackTrace.TrimEnd('\r', '\n') + + Environment.NewLine + + "--- End of stack trace from previous location where exception was thrown ---" + + Environment.NewLine; + remoteField.SetValue(source, remoteStackTrace); + return source; + } var field = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); field?.SetValue(source, stackTrace); return source; diff --git a/src/Split/net471/ExceptionDispatchInfoPolyfill.cs b/src/Split/net471/ExceptionDispatchInfoPolyfill.cs index eb978447..62dc9653 100644 --- a/src/Split/net471/ExceptionDispatchInfoPolyfill.cs +++ b/src/Split/net471/ExceptionDispatchInfoPolyfill.cs @@ -19,6 +19,17 @@ public static Exception SetCurrentStackTrace(Exception source) { if (source == null) throw new ArgumentNullException(nameof(source)); var stackTrace = new StackTrace(1, true).ToString(); + var remoteField = typeof(Exception).GetField("_remoteStackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); + if (remoteField != null) + { + var remoteStackTrace = + stackTrace.TrimEnd('\r', '\n') + + Environment.NewLine + + "--- End of stack trace from previous location where exception was thrown ---" + + Environment.NewLine; + remoteField.SetValue(source, remoteStackTrace); + return source; + } var field = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); field?.SetValue(source, stackTrace); return source; diff --git a/src/Split/net472/ExceptionDispatchInfoPolyfill.cs b/src/Split/net472/ExceptionDispatchInfoPolyfill.cs index eb978447..62dc9653 100644 --- a/src/Split/net472/ExceptionDispatchInfoPolyfill.cs +++ b/src/Split/net472/ExceptionDispatchInfoPolyfill.cs @@ -19,6 +19,17 @@ public static Exception SetCurrentStackTrace(Exception source) { if (source == null) throw new ArgumentNullException(nameof(source)); var stackTrace = new StackTrace(1, true).ToString(); + var remoteField = typeof(Exception).GetField("_remoteStackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); + if (remoteField != null) + { + var remoteStackTrace = + stackTrace.TrimEnd('\r', '\n') + + Environment.NewLine + + "--- End of stack trace from previous location where exception was thrown ---" + + Environment.NewLine; + remoteField.SetValue(source, remoteStackTrace); + return source; + } var field = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); field?.SetValue(source, stackTrace); return source; diff --git a/src/Split/net48/ExceptionDispatchInfoPolyfill.cs b/src/Split/net48/ExceptionDispatchInfoPolyfill.cs index eb978447..62dc9653 100644 --- a/src/Split/net48/ExceptionDispatchInfoPolyfill.cs +++ b/src/Split/net48/ExceptionDispatchInfoPolyfill.cs @@ -19,6 +19,17 @@ public static Exception SetCurrentStackTrace(Exception source) { if (source == null) throw new ArgumentNullException(nameof(source)); var stackTrace = new StackTrace(1, true).ToString(); + var remoteField = typeof(Exception).GetField("_remoteStackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); + if (remoteField != null) + { + var remoteStackTrace = + stackTrace.TrimEnd('\r', '\n') + + Environment.NewLine + + "--- End of stack trace from previous location where exception was thrown ---" + + Environment.NewLine; + remoteField.SetValue(source, remoteStackTrace); + return source; + } var field = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); field?.SetValue(source, stackTrace); return source; diff --git a/src/Split/net481/ExceptionDispatchInfoPolyfill.cs b/src/Split/net481/ExceptionDispatchInfoPolyfill.cs index eb978447..62dc9653 100644 --- a/src/Split/net481/ExceptionDispatchInfoPolyfill.cs +++ b/src/Split/net481/ExceptionDispatchInfoPolyfill.cs @@ -19,6 +19,17 @@ public static Exception SetCurrentStackTrace(Exception source) { if (source == null) throw new ArgumentNullException(nameof(source)); var stackTrace = new StackTrace(1, true).ToString(); + var remoteField = typeof(Exception).GetField("_remoteStackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); + if (remoteField != null) + { + var remoteStackTrace = + stackTrace.TrimEnd('\r', '\n') + + Environment.NewLine + + "--- End of stack trace from previous location where exception was thrown ---" + + Environment.NewLine; + remoteField.SetValue(source, remoteStackTrace); + return source; + } var field = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); field?.SetValue(source, stackTrace); return source; diff --git a/src/Split/netcoreapp2.0/ExceptionDispatchInfoPolyfill.cs b/src/Split/netcoreapp2.0/ExceptionDispatchInfoPolyfill.cs index eb978447..62dc9653 100644 --- a/src/Split/netcoreapp2.0/ExceptionDispatchInfoPolyfill.cs +++ b/src/Split/netcoreapp2.0/ExceptionDispatchInfoPolyfill.cs @@ -19,6 +19,17 @@ public static Exception SetCurrentStackTrace(Exception source) { if (source == null) throw new ArgumentNullException(nameof(source)); var stackTrace = new StackTrace(1, true).ToString(); + var remoteField = typeof(Exception).GetField("_remoteStackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); + if (remoteField != null) + { + var remoteStackTrace = + stackTrace.TrimEnd('\r', '\n') + + Environment.NewLine + + "--- End of stack trace from previous location where exception was thrown ---" + + Environment.NewLine; + remoteField.SetValue(source, remoteStackTrace); + return source; + } var field = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); field?.SetValue(source, stackTrace); return source; diff --git a/src/Split/netcoreapp2.1/ExceptionDispatchInfoPolyfill.cs b/src/Split/netcoreapp2.1/ExceptionDispatchInfoPolyfill.cs index eb978447..62dc9653 100644 --- a/src/Split/netcoreapp2.1/ExceptionDispatchInfoPolyfill.cs +++ b/src/Split/netcoreapp2.1/ExceptionDispatchInfoPolyfill.cs @@ -19,6 +19,17 @@ public static Exception SetCurrentStackTrace(Exception source) { if (source == null) throw new ArgumentNullException(nameof(source)); var stackTrace = new StackTrace(1, true).ToString(); + var remoteField = typeof(Exception).GetField("_remoteStackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); + if (remoteField != null) + { + var remoteStackTrace = + stackTrace.TrimEnd('\r', '\n') + + Environment.NewLine + + "--- End of stack trace from previous location where exception was thrown ---" + + Environment.NewLine; + remoteField.SetValue(source, remoteStackTrace); + return source; + } var field = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); field?.SetValue(source, stackTrace); return source; diff --git a/src/Split/netcoreapp2.2/ExceptionDispatchInfoPolyfill.cs b/src/Split/netcoreapp2.2/ExceptionDispatchInfoPolyfill.cs index eb978447..62dc9653 100644 --- a/src/Split/netcoreapp2.2/ExceptionDispatchInfoPolyfill.cs +++ b/src/Split/netcoreapp2.2/ExceptionDispatchInfoPolyfill.cs @@ -19,6 +19,17 @@ public static Exception SetCurrentStackTrace(Exception source) { if (source == null) throw new ArgumentNullException(nameof(source)); var stackTrace = new StackTrace(1, true).ToString(); + var remoteField = typeof(Exception).GetField("_remoteStackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); + if (remoteField != null) + { + var remoteStackTrace = + stackTrace.TrimEnd('\r', '\n') + + Environment.NewLine + + "--- End of stack trace from previous location where exception was thrown ---" + + Environment.NewLine; + remoteField.SetValue(source, remoteStackTrace); + return source; + } var field = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); field?.SetValue(source, stackTrace); return source; diff --git a/src/Split/netcoreapp3.0/ExceptionDispatchInfoPolyfill.cs b/src/Split/netcoreapp3.0/ExceptionDispatchInfoPolyfill.cs index eb978447..62dc9653 100644 --- a/src/Split/netcoreapp3.0/ExceptionDispatchInfoPolyfill.cs +++ b/src/Split/netcoreapp3.0/ExceptionDispatchInfoPolyfill.cs @@ -19,6 +19,17 @@ public static Exception SetCurrentStackTrace(Exception source) { if (source == null) throw new ArgumentNullException(nameof(source)); var stackTrace = new StackTrace(1, true).ToString(); + var remoteField = typeof(Exception).GetField("_remoteStackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); + if (remoteField != null) + { + var remoteStackTrace = + stackTrace.TrimEnd('\r', '\n') + + Environment.NewLine + + "--- End of stack trace from previous location where exception was thrown ---" + + Environment.NewLine; + remoteField.SetValue(source, remoteStackTrace); + return source; + } var field = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); field?.SetValue(source, stackTrace); return source; diff --git a/src/Split/netcoreapp3.1/ExceptionDispatchInfoPolyfill.cs b/src/Split/netcoreapp3.1/ExceptionDispatchInfoPolyfill.cs index eb978447..62dc9653 100644 --- a/src/Split/netcoreapp3.1/ExceptionDispatchInfoPolyfill.cs +++ b/src/Split/netcoreapp3.1/ExceptionDispatchInfoPolyfill.cs @@ -19,6 +19,17 @@ public static Exception SetCurrentStackTrace(Exception source) { if (source == null) throw new ArgumentNullException(nameof(source)); var stackTrace = new StackTrace(1, true).ToString(); + var remoteField = typeof(Exception).GetField("_remoteStackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); + if (remoteField != null) + { + var remoteStackTrace = + stackTrace.TrimEnd('\r', '\n') + + Environment.NewLine + + "--- End of stack trace from previous location where exception was thrown ---" + + Environment.NewLine; + remoteField.SetValue(source, remoteStackTrace); + return source; + } var field = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); field?.SetValue(source, stackTrace); return source; diff --git a/src/Split/netstandard2.0/ExceptionDispatchInfoPolyfill.cs b/src/Split/netstandard2.0/ExceptionDispatchInfoPolyfill.cs index eb978447..62dc9653 100644 --- a/src/Split/netstandard2.0/ExceptionDispatchInfoPolyfill.cs +++ b/src/Split/netstandard2.0/ExceptionDispatchInfoPolyfill.cs @@ -19,6 +19,17 @@ public static Exception SetCurrentStackTrace(Exception source) { if (source == null) throw new ArgumentNullException(nameof(source)); var stackTrace = new StackTrace(1, true).ToString(); + var remoteField = typeof(Exception).GetField("_remoteStackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); + if (remoteField != null) + { + var remoteStackTrace = + stackTrace.TrimEnd('\r', '\n') + + Environment.NewLine + + "--- End of stack trace from previous location where exception was thrown ---" + + Environment.NewLine; + remoteField.SetValue(source, remoteStackTrace); + return source; + } var field = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); field?.SetValue(source, stackTrace); return source; diff --git a/src/Split/netstandard2.1/ExceptionDispatchInfoPolyfill.cs b/src/Split/netstandard2.1/ExceptionDispatchInfoPolyfill.cs index eb978447..62dc9653 100644 --- a/src/Split/netstandard2.1/ExceptionDispatchInfoPolyfill.cs +++ b/src/Split/netstandard2.1/ExceptionDispatchInfoPolyfill.cs @@ -19,6 +19,17 @@ public static Exception SetCurrentStackTrace(Exception source) { if (source == null) throw new ArgumentNullException(nameof(source)); var stackTrace = new StackTrace(1, true).ToString(); + var remoteField = typeof(Exception).GetField("_remoteStackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); + if (remoteField != null) + { + var remoteStackTrace = + stackTrace.TrimEnd('\r', '\n') + + Environment.NewLine + + "--- End of stack trace from previous location where exception was thrown ---" + + Environment.NewLine; + remoteField.SetValue(source, remoteStackTrace); + return source; + } var field = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); field?.SetValue(source, stackTrace); return source; diff --git a/src/Split/uap10.0/ExceptionDispatchInfoPolyfill.cs b/src/Split/uap10.0/ExceptionDispatchInfoPolyfill.cs index eb978447..62dc9653 100644 --- a/src/Split/uap10.0/ExceptionDispatchInfoPolyfill.cs +++ b/src/Split/uap10.0/ExceptionDispatchInfoPolyfill.cs @@ -19,6 +19,17 @@ public static Exception SetCurrentStackTrace(Exception source) { if (source == null) throw new ArgumentNullException(nameof(source)); var stackTrace = new StackTrace(1, true).ToString(); + var remoteField = typeof(Exception).GetField("_remoteStackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); + if (remoteField != null) + { + var remoteStackTrace = + stackTrace.TrimEnd('\r', '\n') + + Environment.NewLine + + "--- End of stack trace from previous location where exception was thrown ---" + + Environment.NewLine; + remoteField.SetValue(source, remoteStackTrace); + return source; + } var field = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); field?.SetValue(source, stackTrace); return source; diff --git a/src/Tests/PolyfillTests_ExceptionDispatchInfo.cs b/src/Tests/PolyfillTests_ExceptionDispatchInfo.cs index 38d5e5b9..f75b6d78 100644 --- a/src/Tests/PolyfillTests_ExceptionDispatchInfo.cs +++ b/src/Tests/PolyfillTests_ExceptionDispatchInfo.cs @@ -10,4 +10,51 @@ public async Task ExceptionDispatchInfoSetCurrentStackTrace() await Assert.That(result).IsEqualTo(exception); await Assert.That(exception.StackTrace).IsNotNull(); } + + [MethodImpl(MethodImplOptions.NoInlining)] + static Exception CaptureStackTraceInNamedMethod() + { + var exception = ExceptionDispatchInfo.SetCurrentStackTrace(new Exception("test")); + // prevent a tail call from removing this frame + GC.KeepAlive(exception); + return exception; + } + + [Test] + public async Task ExceptionDispatchInfoSetCurrentStackTrace_SurvivesThrow() + { + var exception = CaptureStackTraceInNamedMethod(); + await Assert.That(exception.StackTrace!).Contains(nameof(CaptureStackTraceInNamedMethod)); + + string? thrownStackTrace = null; + try + { + throw exception; + } + catch (Exception caught) + { + thrownStackTrace = caught.StackTrace; + } + + await Assert.That(thrownStackTrace!).Contains(nameof(CaptureStackTraceInNamedMethod)); + await Assert.That(thrownStackTrace!).Contains("End of stack trace from previous location"); + } + + [Test] + public async Task ExceptionDispatchInfoSetCurrentStackTrace_SurvivesAwait() + { + var task = Task.FromException(CaptureStackTraceInNamedMethod()); + + string? thrownStackTrace = null; + try + { + await task; + } + catch (Exception caught) + { + thrownStackTrace = caught.StackTrace; + } + + await Assert.That(thrownStackTrace!).Contains(nameof(CaptureStackTraceInNamedMethod)); + } }