diff --git a/README.md b/README.md index 4c2a8e5..1f78bcc 100644 --- a/README.md +++ b/README.md @@ -183,14 +183,37 @@ private void onDeadEvent(DeadEvent deadEvent) { ... } ### Detecting Exceptions Thrown by Event Handlers When an event handler throws an exception, an exception event is dispatched that wraps the original event. -A exception handler is declared as follows: +An exception handler is declared as follows: ```java private void onExceptionEvent(ExceptionEvent ExceptionEvent) { ... } ``` - Both system events reference the event bus that caused them and a warning is logged if they are unhandled. +#### Yeeting Exceptions Out of an Event Handler + +In some cases, a warning about an `Exception` that was thrown in an event handler is not enough, stays unnoticed, or an exception should be catched explicitly. +Event Bus explicitly dispatches no `ExceptionEvent` when an `ExceptionWrapper` exception is thrown and instead simply rethrows it. +`ExceptionWrapper` is an unchecked exception that (as the name says) simply wraps an exception that caused it. +This means the following is possible and results in a normal program exit: +```java +@Event(String.class) +void onString() { + throw new ExceptionWrapper(new RuntimeException("I failed!")); +} + +void helloStackTrace() { + EventBus.getInstance().registerListener(this); + try { + EventBus.getInstance().dispatch("A string!"); + System.exit(-1); + } catch(ExceptionWrapper e) { + e.getCause().printStackTrace(); + System.exit(0); + } +} +``` + ### What About Endless Recursion Caused By Dead Events and Exception Events? As one might imagine, an unhandled dead event would theoretically lead to an endless recursion. diff --git a/core/src/main/java/dev/kske/eventbus/core/EventBus.java b/core/src/main/java/dev/kske/eventbus/core/EventBus.java index 02cf156..83bef93 100644 --- a/core/src/main/java/dev/kske/eventbus/core/EventBus.java +++ b/core/src/main/java/dev/kske/eventbus/core/EventBus.java @@ -112,10 +112,11 @@ public final class EventBus { * * @param event the event to dispatch * @throws EventBusException if an event handler isn't accessible or has an invalid signature + * @throws ExceptionWrapper if it is thrown by an event handler * @throws NullPointerException if the specified event is {@code null} * @since 0.0.1 */ - public void dispatch(Object event) throws EventBusException { + public void dispatch(Object event) { Objects.requireNonNull(event); logger.log(Level.INFO, "Dispatching event {0}", event); @@ -140,6 +141,10 @@ public final class EventBus { // Transparently pass error to the caller throw (Error) e.getCause(); + else if (e.getCause() instanceof ExceptionWrapper) + + // Transparently pass exception wrapper to the caller + throw (ExceptionWrapper) e.getCause(); else if (event instanceof DeadEvent || event instanceof ExceptionEvent) // Warn about system event not being handled @@ -214,7 +219,7 @@ public final class EventBus { * @since 0.0.1 * @see Event */ - public void registerListener(Object listener) throws EventBusException { + public void registerListener(Object listener) { Objects.requireNonNull(listener); if (registeredListeners.contains(listener)) throw new EventBusException(listener + " already registered!"); diff --git a/core/src/main/java/dev/kske/eventbus/core/EventBusException.java b/core/src/main/java/dev/kske/eventbus/core/EventBusException.java index 618a24a..08e7e13 100644 --- a/core/src/main/java/dev/kske/eventbus/core/EventBusException.java +++ b/core/src/main/java/dev/kske/eventbus/core/EventBusException.java @@ -14,13 +14,14 @@ package dev.kske.eventbus.core; */ public final class EventBusException extends RuntimeException { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 7254445250300604449L; /** * Creates a new event bus exception. * * @param message the message to display * @param cause the cause of this exception + * @since 0.0.1 */ public EventBusException(String message, Throwable cause) { super(message, cause); @@ -30,6 +31,7 @@ public final class EventBusException extends RuntimeException { * Creates a new event bus exception. * * @param message the message to display + * @since 0.0.1 */ public EventBusException(String message) { super(message); diff --git a/core/src/main/java/dev/kske/eventbus/core/ExceptionWrapper.java b/core/src/main/java/dev/kske/eventbus/core/ExceptionWrapper.java new file mode 100644 index 0000000..9647284 --- /dev/null +++ b/core/src/main/java/dev/kske/eventbus/core/ExceptionWrapper.java @@ -0,0 +1,24 @@ +package dev.kske.eventbus.core; + +/** + * This unchecked exception acts as a wrapper for an arbitrary exception to prevent an + * {@link ExceptionEvent} from being dispatched. Instead, the wrapped exception is rethrown by + * {@link EventBus#dispatch(Object)}. + * + * @author Kai S. K. Engelbart + * @since 1.2.1 + */ +public final class ExceptionWrapper extends RuntimeException { + + private static final long serialVersionUID = -2016681140617308788L; + + /** + * Creates a new exception wrapper. + * + * @param cause the exception to wrap + * @since 1.2.1 + */ + public ExceptionWrapper(Exception cause) { + super(cause); + } +} diff --git a/core/src/test/java/dev/kske/eventbus/core/ExceptionWrapperTest.java b/core/src/test/java/dev/kske/eventbus/core/ExceptionWrapperTest.java new file mode 100644 index 0000000..56f2bd9 --- /dev/null +++ b/core/src/test/java/dev/kske/eventbus/core/ExceptionWrapperTest.java @@ -0,0 +1,33 @@ +package dev.kske.eventbus.core; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +/** + * Tests the behavior of the event bus when an {@link ExceptionWrapper} is thrown. + * + * @author Kai S. K. Engelbart + * @since 1.2.1 + */ +public class ExceptionWrapperTest { + + EventBus bus = new EventBus(); + String event = "This event will cause an exception"; + + /** + * Tests transparent rethrowing of an exception wrapper by {@link EventBus#dispatch(Object)}. + * + * @since 1.2.1 + */ + @Test + public void testExceptionWrapper() { + bus.registerListener(this); + assertThrows(ExceptionWrapper.class, () -> bus.dispatch(event)); + } + + @Event(String.class) + void onString() { + throw new ExceptionWrapper(new RuntimeException("I failed!")); + } +}