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
on async Main, unhandled exception unregisters before capturing crash #321
on async Main, unhandled exception unregisters before capturing crash #321
Comments
The workaround for the time being is to create a async main: https://github.com/dotnet/csharplang/blob/master/proposals/csharp-7.1/async-main.md static void Main(string[] args)
{
using (SentrySdk.Init("https://xx"))
{
MainAsync().Wait();
}
}
private static async Task MainAsync()
{
throw new Exception("foobar");
await Task.Run(() =>
{
Console.WriteLine("foo");
});
Console.WriteLine("Hello World!");
} |
This doesn't look like a bug in sentry, but more like an expected behavior due to misuse of You can observe this behavior in the following example: void A()
{
throw null;
}
async Task B()
{
throw null;
}
void C()
{
// Will throw immediately
A();
// Will not throw
var task = B();
// Can inspect exception. Awaiting the task will also propagate the exception.
task.Exception;
} Here are the two methods from your original example, decompiled: public static class Program
{
private static void Main(string[] args)
{
IDisposable disposable = SentrySdk.Init();
try
{
throw null;
}
finally
{
if (disposable != null)
{
disposable.Dispose();
}
}
}
} [CompilerGenerated]
private sealed class <Main>d__0 : IAsyncStateMachine
{
public int <>1__state;
public AsyncTaskMethodBuilder <>t__builder;
public string[] args;
private IDisposable <_>5__1;
private void MoveNext()
{
int num = <>1__state;
try
{
<_>5__1 = SentrySdk.Init();
try
{
throw null;
}
finally
{
if (num < 0 && <_>5__1 != null)
{
<_>5__1.Dispose();
}
}
}
catch (Exception exception)
{
<>1__state = -2;
<>t__builder.SetException(exception);
}
}
void IAsyncStateMachine.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
//ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
this.SetStateMachine(stateMachine);
}
}
[AsyncStateMachine(typeof(<Main>d__0))]
[DebuggerStepThrough]
private static Task Main(string[] args)
{
<Main>d__0 stateMachine = new <Main>d__0();
stateMachine.args = args;
stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.<>1__state = -1;
AsyncTaskMethodBuilder <>t__builder = stateMachine.<>t__builder;
<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;
} The latter generates a (useless) state machine and the exception actually gets caught and stored in Note, the compiler also warns when you write code like this, which kind of implies that it's probably a mistake. |
@mattjohnsonpint could u please validate that for us and close it if it's OK? |
I just checked. It's still an issue with the latest version (3.15.0). I'll see what I can figure out. |
A more realistic reproduction: using Sentry;
namespace ConsoleApp1;
public class Program
{
static async Task Main()
{
using var _ = SentrySdk.Init(o =>
{
o.Dsn = // ... my dsn
o.Debug = true;
});
await DoSomethingAsync();
}
static async Task DoSomethingAsync()
{
await Task.Delay(1000);
throw new Exception("Test Exception");
}
} The unhandled exception is thrown, but not captured by the Sentry SDK. |
@Tyrrrz was correct with #321 (comment). It's not caused by Sentry, and I don't think there's much we can do about it. Here is a reproduction that doesn't use Sentry at all: public class Program
{
static async Task Main()
{
using var _ = new MyUnhandledExceptionHandler();
await DoSomethingAsync();
}
static async Task DoSomethingAsync()
{
await Task.Yield();
throw new Exception("Test Exception");
}
}
public class MyUnhandledExceptionHandler : IDisposable
{
public MyUnhandledExceptionHandler()
{
AppDomain.CurrentDomain.UnhandledException += OnCurrentDomainOnUnhandledException;
}
public void Dispose()
{
AppDomain.CurrentDomain.UnhandledException -= OnCurrentDomainOnUnhandledException;
}
private void OnCurrentDomainOnUnhandledException(object sender, UnhandledExceptionEventArgs args)
{
if (args.ExceptionObject is Exception e)
Console.WriteLine($"*** Caught {e.Message} ***");
}
} The message is never printed because the handler is disposed and unregistered before the task unwinds. Replace the static void Main()
{
using var _ = new MyUnhandledExceptionHandler();
MainAsync().GetAwaiter().GetResult();
}
static async Task MainAsync()
{
await DoSomethingAsync();
} This appears to be by design of .NET, but I will ask on the dotnet/runtime repo. We should also add a note in our SDK docs about this. Also note that @kkl-acies showed using |
I asked at dotnet/runtime#67407 |
And they answered. As suspected, it's by design of .NET. The exception is caught and stored with the task, then the handler is disposed, then the task unwraps and the exception is thrown without anything registered to catch it. I'll add a note to the docs. |
We can improve this by making sure IDisposable doesn't close up the SDK (and unregisters the AppDomain unhandled exception). Even though the Dispose logic of a lot of stuff is still in there, it's not supposed to be called anymore by the Disposable returned by Init. That's because of the changed done here (#1354) for this request: #599 It's possibly we missed something, but if we make the Disposable not dispose anything anymore we could work around this design flaw |
This exception is captured as expected by the unhandled exception handler.
The code above doesn't work as expected. Before calling the
UnhandledException
handler the SDK goes out of theusing
"block" and unregisters the unhandled exception integration.The text was updated successfully, but these errors were encountered: