Skip to content

feat(core): Add ExceptionUtils.handleFatal to rethrow non-recoverable throwables - #5907

Open
markushi wants to merge 1 commit into
mainfrom
feat/rethrow-fatal-exception-util
Open

feat(core): Add ExceptionUtils.handleFatal to rethrow non-recoverable throwables#5907
markushi wants to merge 1 commit into
mainfrom
feat/rethrow-fatal-exception-util

Conversation

@markushi

@markushi markushi commented Aug 7, 2026

Copy link
Copy Markdown
Member

Summary

Adds ExceptionUtils.handleFatal(Throwable), a small utility for use inside broad catch (Throwable t) blocks. It rethrows VirtualMachineError (e.g. OutOfMemoryError, StackOverflowError) and ThreadDeath as-is, and restores the thread's interrupt flag for InterruptedException instead of swallowing it. All other throwables are left untouched for the caller to handle as before.

Closes #5865

#skip-changelog

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor
Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against 6f3de1f

@sentry

sentry Bot commented Aug 7, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
SDK Size io.sentry.tests.size 8.52.0 (1) release

⚙️ sentry-android Build Distribution Settings

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 324.29 ms 380.80 ms 56.51 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
d15471f 315.61 ms 360.22 ms 44.61 ms
bb0ff41 344.70 ms 413.82 ms 69.12 ms
b0aa73e 320.00 ms 371.82 ms 51.82 ms
8558cac 306.16 ms 355.24 ms 49.09 ms
ae7fed0 293.84 ms 380.22 ms 86.38 ms
5b66efd 308.67 ms 363.85 ms 55.18 ms
ee747ae 400.46 ms 423.61 ms 23.15 ms
02e6bc8 396.57 ms 469.08 ms 72.51 ms
11f90db 314.26 ms 372.43 ms 58.17 ms
e63ad34 297.04 ms 369.90 ms 72.86 ms

App size

Revision Plain With Sentry Diff
d15471f 1.58 MiB 2.13 MiB 559.54 KiB
bb0ff41 0 B 0 B 0 B
b0aa73e 0 B 0 B 0 B
8558cac 0 B 0 B 0 B
ae7fed0 1.58 MiB 2.12 MiB 551.77 KiB
5b66efd 1.58 MiB 2.13 MiB 559.07 KiB
ee747ae 1.58 MiB 2.10 MiB 530.95 KiB
02e6bc8 0 B 0 B 0 B
11f90db 0 B 0 B 0 B
e63ad34 0 B 0 B 0 B

if (throwable instanceof VirtualMachineError || throwable instanceof ThreadDeath) {
throw (Error) throwable;
}
if (throwable instanceof InterruptedException) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I was a bit torn apart if this utility method should take care of interrupts as well, and decided to do it, as it will keep all call sites more tight. Along with that change the method is now a generic handleFatal instead of a rethrowFatal. That doesn't tell a lot about the method, but I guess it will be a common pattern everywhere. - Happy for any opinions on this!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems reasonable to me, and definitely nice to have a common facility for restoring the interrupt flag.

The only potential concern I can think of are methods that declare throws InterruptedException and need to use handleFatal(), as sometimes clients might expect either an InterruptedException or an interrupted flag to be set, but not both. But those methods could simply re-throw the InterruptedException before calling handleFatal().

The alternative would be to have separate methods similar to what Project Reactor does:

  • ExceptionUtils.rethrowIfJvmFatal()
  • ExceptionUtils.restoreInterruptIfNeeded()
  • ExceptionUtils.rethrowIfCancellation() (for coroutine CancellationException)

But I prefer the current all-in-one approach more 👍

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That's some good input! We could improve the ergonomics slightly by introducing method chaining. E.g.

ExceptionUtils.restoreInterrupt().rethrowIfJvmFatal()

Let me task the clanker to have a look at this and the actual impact on our codebase

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.

I think it depends on the codebase and if we have use cases where we would need to do one and not the other. Can you ask the clanker to give you a report if we have such cases?

@markushi
markushi marked this pull request as ready for review August 7, 2026 07:33

@0xadam-brown 0xadam-brown left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice – and thanks for this!

One renaming comment for your consideration, but no blockers 🥇

*
* @param throwable - the throwable to check
*/
public static void handleFatal(final @NotNull Throwable throwable) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

l: Thoughts about rethrowIfFatal()?

To me, that'd make its purpose more obvious at the call site (and I'm fine with its cheating a bit w/r/t restoring the interrupted flag).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah sounds good to me too, let's cheat a little 😅

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.

Agreed with rethrowIfFatal!

if (throwable instanceof VirtualMachineError || throwable instanceof ThreadDeath) {
throw (Error) throwable;
}
if (throwable instanceof InterruptedException) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems reasonable to me, and definitely nice to have a common facility for restoring the interrupt flag.

The only potential concern I can think of are methods that declare throws InterruptedException and need to use handleFatal(), as sometimes clients might expect either an InterruptedException or an interrupted flag to be set, but not both. But those methods could simply re-throw the InterruptedException before calling handleFatal().

The alternative would be to have separate methods similar to what Project Reactor does:

  • ExceptionUtils.rethrowIfJvmFatal()
  • ExceptionUtils.restoreInterruptIfNeeded()
  • ExceptionUtils.rethrowIfCancellation() (for coroutine CancellationException)

But I prefer the current all-in-one approach more 👍

public static void handleFatal(final @NotNull Throwable throwable) {
// VirtualMachineError covers OutOfMemoryError, StackOverflowError, InternalError, and
// UnknownError
if (throwable instanceof VirtualMachineError || throwable instanceof ThreadDeath) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(Agree that we should omit re-throwing LinkageError b/c of the compileOnly issue illustrated here.)

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.

That's a valid point but I think we should re-throw LinkageError and only catch it where we expect it to be thrown as you did there.

@runningcode runningcode 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.

Thanks for doing this! It looks good so far.

*
* @param throwable - the throwable to check
*/
public static void handleFatal(final @NotNull Throwable throwable) {

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.

Agreed with rethrowIfFatal!

if (throwable instanceof VirtualMachineError || throwable instanceof ThreadDeath) {
throw (Error) throwable;
}
if (throwable instanceof InterruptedException) {

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.

I think it depends on the codebase and if we have use cases where we would need to do one and not the other. Can you ask the clanker to give you a report if we have such cases?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create a rethrow function to properly handle non-recoverable Throwables

3 participants