Skip to content

Commit 28bd2a8

Browse files
Async void helper (#3379)
1 parent d2505f2 commit 28bd2a8

9 files changed

+92
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
- SentryOptions.EnableTracing has been marked as Obsolete ([#3381](https://github.com/getsentry/sentry-dotnet/pull/3381))
88

9+
### Features
10+
11+
- Added an `SentrySdk.RunAsyncVoid` helper method that lets you capture exceptions from `async void` methods ([#3379](https://github.com/getsentry/sentry-dotnet/pull/3379))
12+
913
### Fixes
1014

1115
- P/Invoke warning for GetWindowThreadProcessId no longer shows when using Sentry in UWP applications ([#3372](https://github.com/getsentry/sentry-dotnet/pull/3372))

samples/Sentry.Samples.Maui/MainPage.xaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@
7070
Clicked="OnNativeCrashClicked"
7171
HorizontalOptions="Center" />
7272

73+
<Button
74+
x:Name="AsyncVoidCrashBtn"
75+
Text="Capture an exception from an async void method"
76+
SemanticProperties.Hint="Throws an exception in an async void event handler."
77+
Clicked="OnAsyncVoidCrashClicked"
78+
HorizontalOptions="Center" />
79+
7380
</VerticalStackLayout>
7481
</ScrollView>
7582

samples/Sentry.Samples.Maui/MainPage.xaml.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,27 @@ private void OnNativeCrashClicked(object sender, EventArgs e)
8787
#pragma warning restore CS0618
8888
#endif
8989
}
90+
91+
private void OnAsyncVoidCrashClicked(object sender, EventArgs e)
92+
{
93+
var client = new HttpClient(new FlakyMessageHandler());
94+
95+
// You can use RunAsyncVoid to call async methods safely from within MAUI event handlers.
96+
SentrySdk.RunAsyncVoid(
97+
async () => await client.GetAsync("https://amostunreliablewebsite.net/"),
98+
ex => _logger.LogWarning(ex, "Error fetching data")
99+
);
100+
101+
// This is an example of the same, omitting any exception handler callback. In this case, the default exception
102+
// handler will be used, which simply captures any exceptions and sends these to Sentry
103+
SentrySdk.RunAsyncVoid(async () => await client.GetAsync("https://amostunreliablewebsite.net/"));
104+
}
105+
106+
private class FlakyMessageHandler : DelegatingHandler
107+
{
108+
protected override Task<HttpResponseMessage> SendAsync(
109+
HttpRequestMessage request,
110+
CancellationToken cancellationToken)
111+
=> throw new Exception();
112+
}
90113
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace Sentry.Internal;
2+
3+
/// <summary>
4+
/// See https://www.jamescrosswell.dev/posts/catching-async-void-exceptions/ for a detailed explanation
5+
/// </summary>
6+
internal class ExceptionHandlingSynchronizationContext(Action<Exception> exceptionHandler, SynchronizationContext? innerContext)
7+
: SynchronizationContext
8+
{
9+
public override void Post(SendOrPostCallback d, object? state)
10+
{
11+
if (state is ExceptionDispatchInfo exceptionInfo)
12+
{
13+
exceptionHandler(exceptionInfo.SourceException);
14+
return;
15+
}
16+
if (innerContext != null)
17+
{
18+
innerContext.Post(d, state);
19+
return;
20+
}
21+
base.Post(d, state);
22+
}
23+
}

src/Sentry/SentrySdk.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,37 @@ public static void PauseSession()
679679
public static void ResumeSession()
680680
=> CurrentHub.ResumeSession();
681681

682+
/// <summary>
683+
/// Runs an `async void` method safely.
684+
/// </summary>
685+
/// <param name="task">Typically either a method group or an async lambda that executes some async void code</param>
686+
/// <param name="handler">
687+
/// An optional callback that will be run if an exception is thrown. If no callback is provided then by default the
688+
/// exception will be captured and sent to Sentry.
689+
/// </param>
690+
/// <example>
691+
/// <code>
692+
/// SentrySdk.RunAsyncVoid(async () => await MyAsyncMethod(), ex => Console.WriteLine(ex.Message));
693+
/// </code>
694+
/// </example>
695+
public static void RunAsyncVoid(Action task, Action<Exception>? handler = null)
696+
{
697+
var syncCtx = SynchronizationContext.Current;
698+
try
699+
{
700+
handler ??= DefaultExceptionHandler;
701+
SynchronizationContext.SetSynchronizationContext(new ExceptionHandlingSynchronizationContext(handler, syncCtx));
702+
task();
703+
}
704+
finally
705+
{
706+
SynchronizationContext.SetSynchronizationContext(syncCtx);
707+
}
708+
return;
709+
710+
void DefaultExceptionHandler(Exception ex) => CaptureException(ex);
711+
}
712+
682713
/// <summary>
683714
/// Deliberately crashes an application, which is useful for testing and demonstration purposes.
684715
/// </summary>

test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,7 @@ namespace Sentry
806806
public static System.IDisposable PushScope() { }
807807
public static System.IDisposable PushScope<TState>(TState state) { }
808808
public static void ResumeSession() { }
809+
public static void RunAsyncVoid(System.Action task, System.Action<System.Exception>? handler = null) { }
809810
public static void StartSession() { }
810811
public static Sentry.ITransactionTracer StartTransaction(Sentry.ITransactionContext context) { }
811812
public static Sentry.ITransactionTracer StartTransaction(Sentry.ITransactionContext context, System.Collections.Generic.IReadOnlyDictionary<string, object?> customSamplingContext) { }

test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,7 @@ namespace Sentry
806806
public static System.IDisposable PushScope() { }
807807
public static System.IDisposable PushScope<TState>(TState state) { }
808808
public static void ResumeSession() { }
809+
public static void RunAsyncVoid(System.Action task, System.Action<System.Exception>? handler = null) { }
809810
public static void StartSession() { }
810811
public static Sentry.ITransactionTracer StartTransaction(Sentry.ITransactionContext context) { }
811812
public static Sentry.ITransactionTracer StartTransaction(Sentry.ITransactionContext context, System.Collections.Generic.IReadOnlyDictionary<string, object?> customSamplingContext) { }

test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,7 @@ namespace Sentry
808808
public static System.IDisposable PushScope() { }
809809
public static System.IDisposable PushScope<TState>(TState state) { }
810810
public static void ResumeSession() { }
811+
public static void RunAsyncVoid(System.Action task, System.Action<System.Exception>? handler = null) { }
811812
public static void StartSession() { }
812813
public static Sentry.ITransactionTracer StartTransaction(Sentry.ITransactionContext context) { }
813814
public static Sentry.ITransactionTracer StartTransaction(Sentry.ITransactionContext context, System.Collections.Generic.IReadOnlyDictionary<string, object?> customSamplingContext) { }

test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,7 @@ namespace Sentry
803803
public static System.IDisposable PushScope() { }
804804
public static System.IDisposable PushScope<TState>(TState state) { }
805805
public static void ResumeSession() { }
806+
public static void RunAsyncVoid(System.Action task, System.Action<System.Exception>? handler = null) { }
806807
public static void StartSession() { }
807808
public static Sentry.ITransactionTracer StartTransaction(Sentry.ITransactionContext context) { }
808809
public static Sentry.ITransactionTracer StartTransaction(Sentry.ITransactionContext context, System.Collections.Generic.IReadOnlyDictionary<string, object?> customSamplingContext) { }

0 commit comments

Comments
 (0)