Skip to content

[Mono.Android] Fix UnhandledExceptionRaiser not firing in .NET 10 - #10966

Merged
jonathanpeppers merged 3 commits into
mainfrom
dev/peppers/unhandledexceptions
Mar 19, 2026
Merged

[Mono.Android] Fix UnhandledExceptionRaiser not firing in .NET 10#10966
jonathanpeppers merged 3 commits into
mainfrom
dev/peppers/unhandledexceptions

Conversation

@jonathanpeppers

Copy link
Copy Markdown
Member

Context: dotnet/java-interop#1275
Fixes: https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2842209

dotnet/java-interop#1275 eliminated JNINativeWrapper.CreateDelegate() from generated binding marshal methods. The old code path routed exceptions through AndroidEnvironment.UnhandledException(), which fires the UnhandledExceptionRaiser event. The new code path calls JniRuntime.OnUserUnhandledException(), which only calls SetPendingException() and never invokes AndroidEnvironment.UnhandledException().

Fix by overriding OnUserUnhandledException() in AndroidRuntime to call AndroidEnvironment.TryRaiseUnhandledException() before delegating to the base implementation. If a subscriber sets Handled = true, the exception is swallowed and not transitioned to JNI.

Refactor AndroidEnvironment.UnhandledException() to extract the event- raising logic into TryRaiseUnhandledException() so it can be called from both the old and new code paths.

Tests

Add a test that verifies both UnhandledExceptionRaiser and AppDomain.UnhandledException fire when an unhandled exception is thrown from a button click handler.

The test:

  1. Registers both UnhandledExceptionRaiser (with e.Handled = true) and AppDomain.UnhandledException handlers
  2. Throws from the button click delegate
  3. Asserts UnhandledExceptionRaiser fires via logcat

Context: dotnet/java-interop#1275
Fixes: https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2842209

dotnet/java-interop#1275 eliminated
`JNINativeWrapper.CreateDelegate()` from generated binding marshal
methods. The old code path routed exceptions through
`AndroidEnvironment.UnhandledException()`, which fires the
`UnhandledExceptionRaiser` event. The new code path calls
`JniRuntime.OnUserUnhandledException()`, which only calls
`SetPendingException()` and never invokes
`AndroidEnvironment.UnhandledException()`.

Fix by overriding `OnUserUnhandledException()` in `AndroidRuntime` to
call `AndroidEnvironment.TryRaiseUnhandledException()` before
delegating to the base implementation. If a subscriber sets `Handled =
true`, the exception is swallowed and not transitioned to JNI.

Refactor `AndroidEnvironment.UnhandledException()` to extract the
event- raising logic into `TryRaiseUnhandledException()` so it can be
called from both the old and new code paths.

~~ Tests ~~

Add a test that verifies both `UnhandledExceptionRaiser` and
`AppDomain.UnhandledException` fire when an unhandled exception is
thrown from a button click handler.

The test:

1. Registers both `UnhandledExceptionRaiser` (with `e.Handled = true`)
   and `AppDomain.UnhandledException` handlers
2. Throws from the button click delegate
3. Asserts `UnhandledExceptionRaiser` fires via logcat

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 18, 2026 16:11
@jonathanpeppers jonathanpeppers added the copilot `copilot-cli` or other AIs were used to author this label Mar 18, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a regression where managed exceptions thrown from generated binding marshal methods no longer trigger AndroidEnvironment.UnhandledExceptionRaiser on .NET 10+ (after the switch away from JNINativeWrapper.CreateDelegate()), by routing the new JniRuntime.OnUserUnhandledException() path through the same event-raising mechanism.

Changes:

  • Override AndroidRuntime.OnUserUnhandledException() to invoke AndroidEnvironment.TryRaiseUnhandledException() and swallow the exception when Handled=true.
  • Refactor AndroidEnvironment.UnhandledException() to extract reusable event-raising logic into TryRaiseUnhandledException().
  • Add a device integration test that throws from a button click handler and validates logcat output for UnhandledExceptionRaiser.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs Adds a device test that throws from a UI click handler and checks logcat for the raised unhandled-exception event.
src/Mono.Android/Android.Runtime/AndroidRuntime.cs Overrides OnUserUnhandledException() to raise UnhandledExceptionRaiser for the new Java.Interop exception path.
src/Mono.Android/Android.Runtime/AndroidEnvironment.cs Extracts event-raising into TryRaiseUnhandledException() so both legacy and new paths can share it.

Comment thread tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs Outdated
Comment thread tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs Outdated
Comment thread src/Mono.Android/Android.Runtime/AndroidRuntime.cs Outdated
- Assert WaitForActivityToStart return value
- Remove unused AppDomain.UnhandledException handler (it won't fire
  when UnhandledExceptionRaiser sets Handled = true)
- Fix misleading comment in OnUserUnhandledException override

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers
jonathanpeppers force-pushed the dev/peppers/unhandledexceptions branch from c61e4b7 to 966b26f Compare March 18, 2026 16:23
@jonathanpeppers
jonathanpeppers merged commit 2ccdb27 into main Mar 19, 2026
5 of 6 checks passed
@jonathanpeppers
jonathanpeppers deleted the dev/peppers/unhandledexceptions branch March 19, 2026 12:29
jonathanpeppers added a commit that referenced this pull request Mar 19, 2026
…0966)

Context: dotnet/java-interop#1275
Fixes: https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2842209

dotnet/java-interop#1275 eliminated
`JNINativeWrapper.CreateDelegate()` from generated binding marshal
methods. The old code path routed exceptions through
`AndroidEnvironment.UnhandledException()`, which fires the
`UnhandledExceptionRaiser` event. The new code path calls
`JniRuntime.OnUserUnhandledException()`, which only calls
`SetPendingException()` and never invokes
`AndroidEnvironment.UnhandledException()`.

Fix by overriding `OnUserUnhandledException()` in `AndroidRuntime` to
call `AndroidEnvironment.TryRaiseUnhandledException()` before
delegating to the base implementation. If a subscriber sets `Handled =
true`, the exception is swallowed and not transitioned to JNI.

Refactor `AndroidEnvironment.UnhandledException()` to extract the
event- raising logic into `TryRaiseUnhandledException()` so it can be
called from both the old and new code paths.

~~ Tests ~~

Add a test that verifies both `UnhandledExceptionRaiser` and
`AppDomain.UnhandledException` fire when an unhandled exception is
thrown from a button click handler.

The test:

1. Registers both `UnhandledExceptionRaiser` (with `e.Handled = true`)
   and `AppDomain.UnhandledException` handlers
2. Throws from the button click delegate
3. Asserts `UnhandledExceptionRaiser` fires via logcat
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 19, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

copilot `copilot-cli` or other AIs were used to author this

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants