Repository: webforj/webforj
Pull request #897 (2025-02-06 14:48:32)

fix: webforj does not properly clean up the environment after a connection is lost

github-actions[bot]: <h1>Dependency Review</h1>
✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.<h2>OpenSSF Scorecard</h2>
<table><tr><th>Package</th><th>Version</th><th>Score</th><th>Details</th></tr>
</table><h2>Scanned Files</h2>
<ul>


<!-- dependency-review-pr-comment-marker -->
github-actions[bot]: ### Code Coverage
|Overall Project|71.83% **`-0.57%`**|:green_apple:|
|:-|:-|:-:|
|Files changed|36.59%|:x:|
<br>

|Module|Coverage||
|:-|:-|:-:|
|webforj-data|85.39% **`-0.05%`**|:green_apple:|
|webforj-table|76.66% **`-0.74%`**|:x:|
|webforj-foundation|68% **`-0.75%`**|:x:|

<details>
<summary>Files</summary>

|Module|File|Coverage||
|:-|:-|:-|:-:|
|webforj-data|[EntityKeysRegistry.java](https://github.com/webforj/webforj/blob/2729ceb70a5c5ccc1b7506198a1cc90e9b7e1f86/webforj-data%2Fsrc%2Fmain%2Fjava%2Fcom%2Fwebforj%2Fdata%2FEntityKeysRegistry.java)|98.23% **`-1.77%`**|:green_apple:|
|webforj-table|[Table.java](https://github.com/webforj/webforj/blob/2729ceb70a5c5ccc1b7506198a1cc90e9b7e1f86/webforj-components%2Fwebforj-table%2Fsrc%2Fmain%2Fjava%2Fcom%2Fwebforj%2Fcomponent%2Ftable%2FTable.java)|77.81% **`-1.58%`**|:x:|
|webforj-foundation|[SlotRegistry.java](https://github.com/webforj/webforj/blob/2729ceb70a5c5ccc1b7506198a1cc90e9b7e1f86/webforj-foundation%2Fsrc%2Fmain%2Fjava%2Fcom%2Fwebforj%2Fcomponent%2FSlotRegistry.java)|100%|:green_apple:|
||[ElementComposite.java](https://github.com/webforj/webforj/blob/2729ceb70a5c5ccc1b7506198a1cc90e9b7e1f86/webforj-foundation%2Fsrc%2Fmain%2Fjava%2Fcom%2Fwebforj%2Fcomponent%2Felement%2FElementComposite.java)|93.88%|:green_apple:|
||[EventSinkListenerRegistry.java](https://github.com/webforj/webforj/blob/2729ceb70a5c5ccc1b7506198a1cc90e9b7e1f86/webforj-foundation%2Fsrc%2Fmain%2Fjava%2Fcom%2Fwebforj%2Fcomponent%2Fevent%2FEventSinkListenerRegistry.java)|93.21%|:green_apple:|
||[DwcComponent.java](https://github.com/webforj/webforj/blob/2729ceb70a5c5ccc1b7506198a1cc90e9b7e1f86/webforj-foundation%2Fsrc%2Fmain%2Fjava%2Fcom%2Fwebforj%2Fcomponent%2FDwcComponent.java)|88.88%|:green_apple:|
||[MemoryHistory.java](https://github.com/webforj/webforj/blob/2729ceb70a5c5ccc1b7506198a1cc90e9b7e1f86/webforj-foundation%2Fsrc%2Fmain%2Fjava%2Fcom%2Fwebforj%2Frouter%2Fhistory%2FMemoryHistory.java)|84.7% **`-1.78%`**|:x:|
||[Element.java](https://github.com/webforj/webforj/blob/2729ceb70a5c5ccc1b7506198a1cc90e9b7e1f86/webforj-foundation%2Fsrc%2Fmain%2Fjava%2Fcom%2Fwebforj%2Fcomponent%2Felement%2FElement.java)|84.18%|:green_apple:|
||[Router.java](https://github.com/webforj/webforj/blob/2729ceb70a5c5ccc1b7506198a1cc90e9b7e1f86/webforj-foundation%2Fsrc%2Fmain%2Fjava%2Fcom%2Fwebforj%2Frouter%2FRouter.java)|82.8% **`-1.15%`**|:x:|
||[ObjectTable.java](https://github.com/webforj/webforj/blob/2729ceb70a5c5ccc1b7506198a1cc90e9b7e1f86/webforj-foundation%2Fsrc%2Fmain%2Fjava%2Fcom%2Fwebforj%2Fenvironment%2FObjectTable.java)|78.26% **`-11.59%`**|:green_apple:|
||[Environment.java](https://github.com/webforj/webforj/blob/2729ceb70a5c5ccc1b7506198a1cc90e9b7e1f86/webforj-foundation%2Fsrc%2Fmain%2Fjava%2Fcom%2Fwebforj%2FEnvironment.java)|69.82% **`-0.36%`**|:green_apple:|
||[StringTable.java](https://github.com/webforj/webforj/blob/2729ceb70a5c5ccc1b7506198a1cc90e9b7e1f86/webforj-foundation%2Fsrc%2Fmain%2Fjava%2Fcom%2Fwebforj%2Fenvironment%2FStringTable.java)|63.79% **`-22.41%`**|:green_apple:|
||[Window.java](https://github.com/webforj/webforj/blob/2729ceb70a5c5ccc1b7506198a1cc90e9b7e1f86/webforj-foundation%2Fsrc%2Fmain%2Fjava%2Fcom%2Fwebforj%2Fcomponent%2Fwindow%2FWindow.java)|62.32% **`-14.49%`**|:x:|
||[BrowserConsole.java](https://github.com/webforj/webforj/blob/2729ceb70a5c5ccc1b7506198a1cc90e9b7e1f86/webforj-foundation%2Fsrc%2Fmain%2Fjava%2Fcom%2Fwebforj%2FBrowserConsole.java)|50.17% **`-3.25%`**|:x:|
||[Page.java](https://github.com/webforj/webforj/blob/2729ceb70a5c5ccc1b7506198a1cc90e9b7e1f86/webforj-foundation%2Fsrc%2Fmain%2Fjava%2Fcom%2Fwebforj%2FPage.java)|44.96% **`-4.44%`**|:x:|
||[PageUnloadEventHandler.java](https://github.com/webforj/webforj/blob/2729ceb70a5c5ccc1b7506198a1cc90e9b7e1f86/webforj-foundation%2Fsrc%2Fmain%2Fjava%2Fcom%2Fwebforj%2Fevent%2Fpage%2FPageUnloadEventHandler.java)|42.86% **`-14.29%`**|:x:|
||[App.java](https://github.com/webforj/webforj/blob/2729ceb70a5c5ccc1b7506198a1cc90e9b7e1f86/webforj-foundation%2Fsrc%2Fmain%2Fjava%2Fcom%2Fwebforj%2FApp.java)|26% **`-7.02%`**|:x:|
||[WebforjWebManagerException.java](https://github.com/webforj/webforj/blob/2729ceb70a5c5ccc1b7506198a1cc90e9b7e1f86/webforj-foundation%2Fsrc%2Fmain%2Fjava%2Fcom%2Fwebforj%2Fexceptions%2FWebforjWebManagerException.java)|25%|:green_apple:|
||[GlobalErrorHandler.java](https://github.com/webforj/webforj/blob/2729ceb70a5c5ccc1b7506198a1cc90e9b7e1f86/webforj-foundation%2Fsrc%2Fmain%2Fjava%2Fcom%2Fwebforj%2Ferror%2FGlobalErrorHandler.java)|23.53% **`-17.65%`**|:x:|
||[History.java](https://github.com/webforj/webforj/blob/2729ceb70a5c5ccc1b7506198a1cc90e9b7e1f86/webforj-foundation%2Fsrc%2Fmain%2Fjava%2Fcom%2Fwebforj%2Frouter%2Fhistory%2FHistory.java)|22.22%|:green_apple:|
||[ErrorHandler.java](https://github.com/webforj/webforj/blob/2729ceb70a5c5ccc1b7506198a1cc90e9b7e1f86/webforj-foundation%2Fsrc%2Fmain%2Fjava%2Fcom%2Fwebforj%2Ferror%2FErrorHandler.java)|0% **`-96.15%`**|:x:|
||[Bootstrap.java](https://github.com/webforj/webforj/blob/2729ceb70a5c5ccc1b7506198a1cc90e9b7e1f86/webforj-foundation%2Fsrc%2Fmain%2Fjava%2Fcom%2Fwebforj%2FBootstrap.java)|0% **`-1.81%`**|:x:|
||[BrowserHistory.java](https://github.com/webforj/webforj/blob/2729ceb70a5c5ccc1b7506198a1cc90e9b7e1f86/webforj-foundation%2Fsrc%2Fmain%2Fjava%2Fcom%2Fwebforj%2Frouter%2Fhistory%2FBrowserHistory.java)|0% **`-62.5%`**|:x:|

</details>

Patch:
diff --git a/webforj-components/webforj-table/src/main/java/com/webforj/component/table/Table.java b/webforj-components/webforj-table/src/main/java/com/webforj/component/table/Table.java
index 9fbf8807..08b07273 100644
--- a/webforj-components/webforj-table/src/main/java/com/webforj/component/table/Table.java
+++ b/webforj-components/webforj-table/src/main/java/com/webforj/component/table/Table.java
@@ -991,6 +991,19 @@ public final class Table<T> extends HtmlComponent<Table<T>> implements HasReposi
     return addSortChangeListener(listener);
   }
 
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  protected void onDidDestroy() {
+    super.onDidDestroy();
+    getItemKeysRegistry().cleanUp();
+    columns.clear();
+    selectedKeys.clear();
+    orderCriteriaList.clear();
+    repository = null;
+  }
+
   void onInit(Element el) {
     this.refresh();
     set(selectedProp, selectedKeys);
diff --git a/webforj-data/src/main/java/com/webforj/data/EntityKeysRegistry.java b/webforj-data/src/main/java/com/webforj/data/EntityKeysRegistry.java
index 32d53cd0..6328a608 100644
--- a/webforj-data/src/main/java/com/webforj/data/EntityKeysRegistry.java
+++ b/webforj-data/src/main/java/com/webforj/data/EntityKeysRegistry.java
@@ -101,4 +101,25 @@ public class EntityKeysRegistry {
 
     return getKey(entity);
   }
+
+  /**
+   * Removes the specified entity and its associated key from the registry.
+   *
+   * @param entity the entity to be removed. Must not be null.
+   */
+  public void removeEntity(Object entity) {
+    Objects.requireNonNull(entity, "Entity cannot be null");
+
+    String key = entityToKeyMap.remove(entity);
+    if (key != null) {
+      keyToEntityMap.remove(key);
+    }
+  }
+
+  /**
+   * Cleans up the stale entries in the registry.
+   */
+  public void cleanUp() {
+    keyToEntityMap.keySet().removeIf(key -> !entityToKeyMap.containsValue(key));
+  }
 }
diff --git a/webforj-foundation/src/main/java/com/webforj/App.java b/webforj-foundation/src/main/java/com/webforj/App.java
index 9d49fdd5..6d67b4ad 100644
--- a/webforj-foundation/src/main/java/com/webforj/App.java
+++ b/webforj-foundation/src/main/java/com/webforj/App.java
@@ -31,7 +31,6 @@ import java.net.URL;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
-import java.util.logging.Logger;
 
 /**
  * This is the central class representing an app. In order to implement an app, extend this class
@@ -40,8 +39,6 @@ import java.util.logging.Logger;
  */
 @SuppressWarnings("java:S1610") // we want this to be abstract class, not interface
 public abstract class App {
-  private static final Logger log = Logger.getLogger("com.webforj.App");
-
   /**
    * A default app action is to clear the browser and display a localized message of "Click to
    * reload application", with a link to the application when the application is terminated or error
@@ -90,8 +87,10 @@ public abstract class App {
       resolveFirstRoute();
       isInitialized = true;
       onDidRun();
-    } catch (WebforjWebManagerException ex) {
-      log.severe("Failed to initialize the app. Web Manager API are not available.");
+    } catch (Exception e) {
+      if (!isCausedByChannelTermination(e)) {
+        throw e;
+      }
     }
   }
 
@@ -488,7 +487,27 @@ public abstract class App {
    */
   public final void terminate() {
     onWillTerminate();
+
+    // dispose the page
+    Page page = Page.getCurrent();
+    if (page != null) {
+      page.destroy();
+    }
+
+    // dispose the router
+    Router router = Router.getCurrent();
+    if (router != null) {
+      router.removeAllListeners();
+    }
+
+    // dispose the frames
+    List<Frame> frames = getFrames();
+    for (Frame frame : frames) {
+      frame.destroy();
+    }
+
     Environment.getCurrent().getBBjAPI().postPriorityCustomEvent("webforjTerminateSignal", null);
+
     onDidTerminate();
   }
 
@@ -867,6 +886,25 @@ public abstract class App {
     return getClass().isAnnotationPresent(Routify.class);
   }
 
+  /**
+   * Checks if the given exception is caused by a channel termination.
+   *
+   * @param e The exception to check
+   * @return {@code true} if the exception is caused by a channel termination, {@code false}
+   *         otherwise
+   */
+  private static boolean isCausedByChannelTermination(Throwable e) {
+    while (e != null) {
+      if (e instanceof BBjException && "Channel has been terminated".equals(e.getMessage())) {
+        return true;
+      }
+
+      e = e.getCause();
+    }
+
+    return false;
+  }
+
   Environment getEnvironment() {
     return Environment.getCurrent();
   }
diff --git a/webforj-foundation/src/main/java/com/webforj/Bootstrap.java b/webforj-foundation/src/main/java/com/webforj/Bootstrap.java
index b8bba0bb..cfa7d17d 100644
--- a/webforj-foundation/src/main/java/com/webforj/Bootstrap.java
+++ b/webforj-foundation/src/main/java/com/webforj/Bootstrap.java
@@ -62,23 +62,28 @@ public final class Bootstrap {
    * @param className the fully qualified class name of the application to launch, or {@code null}
    *        to automatically detect it.
    *
+   * @return the application instance.
+   *
    * @throws BBjException if there is an error initializing the environment.
    * @throws WebforjException if there is an error initializing the application, such as if no valid
    *         application class is found or if multiple annotated application classes are detected.
    */
-  public static void init(BBjAPI api, WebforjBBjBridge bridge, int debug, String className)
+  public static App init(BBjAPI api, WebforjBBjBridge bridge, int debug, String className)
       throws BBjException, WebforjException {
     Environment.init(api, bridge, debug);
     processConfig();
-    initApplication(className);
+    return initApplication(className);
   }
 
   /**
    * Launches the main application.
    *
    * @param className the fully qualified class name of the application to launch, or {@code null}
+   * @return the application instance.
+   *
+   * @throws WebforjException if there is an error initializing the application.
    */
-  private static void initApplication(String className) throws WebforjException {
+  private static App initApplication(String className) throws WebforjException {
     String selectedClassName = detectClassName(className);
 
     if (selectedClassName == null || selectedClassName.isEmpty()) {
@@ -91,6 +96,7 @@ public final class Bootstrap {
       App app = ConceiverProvider.getCurrent()
           .getApplication((Class<? extends App>) Class.forName(selectedClassName));
       app.initialize();
+      return app;
     } catch (ClassNotFoundException e) {
       throw new WebforjAppInitializeException("Failed to find application class '"
           + selectedClassName + "'." + " Ensure the class is in the classpath.", e);
diff --git a/webforj-foundation/src/main/java/com/webforj/BrowserConsole.java b/webforj-foundation/src/main/java/com/webforj/BrowserConsole.java
index 4a5def60..dca30015 100644
--- a/webforj-foundation/src/main/java/com/webforj/BrowserConsole.java
+++ b/webforj-foundation/src/main/java/com/webforj/BrowserConsole.java
@@ -603,7 +603,8 @@ public final class BrowserConsole {
 
   private BrowserConsole print(Object message, String level, boolean forced) {
     if (!forced) {
-      boolean isDebug = Environment.getCurrent().isDebug();
+      Environment env = Environment.getCurrent();
+      boolean isDebug = env != null && env.isDebug();
       if (!isDebug) {
         return this;
       }
@@ -622,7 +623,7 @@ public final class BrowserConsole {
     message = Base64.getEncoder().encodeToString(theMessage.getBytes());
     String code = String.format("console.%s('%%c ' + atob('%s'), '%s')", level, message,
         buildComputedStyles());
-    Page.getCurrent().executeJsAsync(code);
+    Page.ifPresent(page -> page.executeJsAsync(code));
     styles.clear();
     return this;
   }
diff --git a/webforj-foundation/src/main/java/com/webforj/Environment.java b/webforj-foundation/src/main/java/com/webforj/Environment.java
index 909e2cfd..65d642be 100644
--- a/webforj-foundation/src/main/java/com/webforj/Environment.java
+++ b/webforj-foundation/src/main/java/com/webforj/Environment.java
@@ -15,6 +15,7 @@ import java.nio.file.Paths;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.ServiceLoader;
+import java.util.function.Consumer;
 
 /**
  * The Environment class represents the runtime environment for the webforj application. It provides
@@ -83,6 +84,27 @@ public final class Environment {
     return Environment.instanceMap.get(Thread.currentThread().getId());
   }
 
+  /**
+   * Check if the environment is present.
+   *
+   * @return true if the environment is present
+   */
+  public static boolean isPresent() {
+    return getCurrent() != null;
+  }
+
+  /**
+   * Executes the given consumer with the environment for the current thread when it is available.
+   *
+   * @param consumer the consumer to execute
+   * @since 24.22
+   */
+  public static void ifPresent(Consumer<Environment> consumer) {
+    if (isPresent()) {
+      consumer.accept(getCurrent());
+    }
+  }
+
   /**
    * Enables or disables debug mode for the current environment.
    *
diff --git a/webforj-foundation/src/main/java/com/webforj/Page.java b/webforj-foundation/src/main/java/com/webforj/Page.java
index 2dc13a88..1f2bfe51 100644
--- a/webforj-foundation/src/main/java/com/webforj/Page.java
+++ b/webforj-foundation/src/main/java/com/webforj/Page.java
@@ -25,12 +25,14 @@ import com.webforj.utilities.Assets;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.lang.System.Logger;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
 import java.nio.file.StandardOpenOption;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.function.Consumer;
 
 /**
  * Represents the web page open in the browser where the app is running and provides methods to
@@ -40,12 +42,12 @@ import java.util.Map;
  * @since 23.00
  */
 public final class Page implements HasJsExecution {
+  private static Logger logger = System.getLogger(Page.class.getName());
   public static final String DEFAULT_TITLE_FORMAT =
       "{WindowTitle ?? ''} {BrowserTitle ? ((WindowTitle ? '- ' : '') + BrowserTitle) : ''}";
   private static final String FAILED_TO_DOWNLOAD_FILE = "Failed to download file.";
   private PageExecuteJsAsyncHandler executeJsAsyncHandler = null;
   private Environment environment = Environment.getCurrent();
-  private EventDispatcher dispatcher = new EventDispatcher();
   private boolean isBrowserCloseEventRegistered = false;
   private final Map<String, PageEventSinkRegistry> registries = new HashMap<>();
   private final EventDispatcher eventDispatcher = new EventDispatcher();
@@ -58,7 +60,7 @@ public final class Page implements HasJsExecution {
    * @return the current page instance
    */
   public static Page getCurrent() {
-    String key = ".page.instance";
+    String key = Page.class.getName();
     if (ObjectTable.contains(key)) {
       return (Page) ObjectTable.get(key);
     }
@@ -69,6 +71,27 @@ public final class Page implements HasJsExecution {
     return instance;
   }
 
+  /**
+   * Check if the page is present.
+   *
+   * @return true if the page is present
+   */
+  public static boolean isPresent() {
+    return getCurrent() != null;
+  }
+
+  /**
+   * Executes the given consumer with the page is present.
+   *
+   * @param consumer the consumer to execute
+   * @since 24.22
+   */
+  public static void ifPresent(Consumer<Page> consumer) {
+    if (Page.isPresent()) {
+      consumer.accept(Page.getCurrent());
+    }
+  }
+
   /**
    * Get the current environment instance.
    *
@@ -85,7 +108,11 @@ public final class Page implements HasJsExecution {
    * @throws BBjException if failed to get the web manager
    */
   BBjWebManager getWebManager() throws BBjException {
-    return getEnvironment().getBBjAPI().getWebManager();
+    try {
+      return getEnvironment().getBBjAPI().getWebManager();
+    } catch (NullPointerException e) {
+      throw new BBjException(e, 0);
+    }
   }
 
   /**
@@ -1011,14 +1038,14 @@ public final class Page implements HasJsExecution {
       try {
         BBjWebManager webManager = getEnvironment().getBBjAPI().getWebManager();
         CustomObject handler = getEnvironment().getBridge()
-            .getEventProxy(new PageUnloadEventHandler(this, dispatcher), "handleEvent");
+            .getEventProxy(new PageUnloadEventHandler(this, getEventDispatcher()), "handleEvent");
         webManager.setCallback(SysGuiEventConstants.ON_BROWSER_CLOSE, handler, "onEvent");
       } catch (BBjException e) {
         throw new WebforjWebManagerException("Failed to register browser close event.", e);
       }
     }
 
-    return dispatcher.addListener(PageUnloadEvent.class, listener);
+    return getEventDispatcher().addListener(PageUnloadEvent.class, listener);
   }
 
   /**
@@ -1032,10 +1059,6 @@ public final class Page implements HasJsExecution {
     return addUnloadListener(listener);
   }
 
-  private EventDispatcher getEventDispatcher() {
-    return eventDispatcher;
-  }
-
   private void performDownload(String sourceFilePath, String fileName) throws BBjException {
     BBjThinClient thinClient = getEnvironment().getBBjAPI().getThinClient();
     BBjClientFileSystem clientFileSystem = thinClient.getClientFileSystem();
@@ -1048,8 +1071,18 @@ public final class Page implements HasJsExecution {
       try {
         Files.delete(tempFilePath);
       } catch (IOException e) {
-        Environment.logError("Failed to delete temp file: " + tempFilePath, e);
+        logger.log(Logger.Level.WARNING, "Failed to delete temp file: " + tempFilePath, e);
       }
     }
   }
+
+  private EventDispatcher getEventDispatcher() {
+    return eventDispatcher;
+  }
+
+  void destroy() {
+    ObjectTable.put(Page.class.getName(), null);
+    getEventDispatcher().removeAllListeners();
+    registries.clear();
+  }
 }
diff --git a/webforj-foundation/src/main/java/com/webforj/component/DwcComponent.java b/webforj-foundation/src/main/java/com/webforj/component/DwcComponent.java
index 11c8be4d..ce50b50a 100644
--- a/webforj-foundation/src/main/java/com/webforj/component/DwcComponent.java
+++ b/webforj-foundation/src/main/java/com/webforj/component/DwcComponent.java
@@ -1156,10 +1156,19 @@ public abstract class DwcComponent<T extends DwcComponent<T>> extends Component
     try {
       if (control != null && !control.isDestroyed()) {
         control.destroy();
-        control = null;
       }
     } catch (BBjException e) {
-      throw new WebforjRuntimeException(e);
+      // pass
+    } finally {
+      setControl(null);
+      attributes.clear();
+      styles.clear();
+      properties.clear();
+      classNames.clear();
+      removedClassNames.clear();
+      removedStyles.clear();
+      slotAssigner.getSlotRegistry().dispose();
+      dispatcher.removeAllListeners();
     }
   }
 
diff --git a/webforj-foundation/src/main/java/com/webforj/component/SlotRegistry.java b/webforj-foundation/src/main/java/com/webforj/component/SlotRegistry.java
index d0e81313..acb9c078 100644
--- a/webforj-foundation/src/main/java/com/webforj/component/SlotRegistry.java
+++ b/webforj-foundation/src/main/java/com/webforj/component/SlotRegistry.java
@@ -193,6 +193,14 @@ public class SlotRegistry {
     return new ArrayList<>(slots.keySet());
   }
 
+  /**
+   * Disposes of all components in all slots and clears the slots.
+   */
+  public void dispose() {
+    slots.values().forEach(components -> components.forEach(Component::destroy));
+    slots.clear();
+  }
+
   /**
    * Resolves the slot name, returning the default slot if the given slot name is null or empty.
    *
diff --git a/webforj-foundation/src/main/java/com/webforj/component/element/Element.java b/webforj-foundation/src/main/java/com/webforj/component/element/Element.java
index d3a6fe07..d5f0471c 100644
--- a/webforj-foundation/src/main/java/com/webforj/component/element/Element.java
+++ b/webforj-foundation/src/main/java/com/webforj/component/element/Element.java
@@ -692,6 +692,18 @@ public final class Element extends DwcContainer<Element>
     }
   }
 
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  protected void onDestroy() {
+    super.onDestroy();
+    isDefined = false;
+    properties.clear();
+    whenDefinedResults.clear();
+    slotRegistry.dispose();
+  }
+
   /**
    * Find the control associated with this component.
    *
diff --git a/webforj-foundation/src/main/java/com/webforj/component/element/ElementComposite.java b/webforj-foundation/src/main/java/com/webforj/component/element/ElementComposite.java
index dd8eda33..03de7d8d 100644
--- a/webforj-foundation/src/main/java/com/webforj/component/element/ElementComposite.java
+++ b/webforj-foundation/src/main/java/com/webforj/component/element/ElementComposite.java
@@ -311,6 +311,20 @@ public abstract class ElementComposite extends Composite<Element> {
     }
   }
 
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  protected void onDidDestroy() {
+    super.onDidDestroy();
+    attributes.clear();
+    attributeTypes.clear();
+    properties.clear();
+    propertyTypes.clear();
+    eventNameToClassMap.clear();
+    dispatcher.removeAllListeners();
+  }
+
   /**
    * Gets the value of a property of the element.
    *
diff --git a/webforj-foundation/src/main/java/com/webforj/component/event/EventSinkListenerRegistry.java b/webforj-foundation/src/main/java/com/webforj/component/event/EventSinkListenerRegistry.java
index 9d8bee23..d55148c0 100644
--- a/webforj-foundation/src/main/java/com/webforj/component/event/EventSinkListenerRegistry.java
+++ b/webforj-foundation/src/main/java/com/webforj/component/event/EventSinkListenerRegistry.java
@@ -73,6 +73,7 @@ public class EventSinkListenerRegistry<E extends EventObject, T> {
    */
   public void attach() {
     registrations.forEach(DwcListenerRegistration::connect);
+    registrations.clear();
   }
 
   /**
diff --git a/webforj-foundation/src/main/java/com/webforj/component/window/Window.java b/webforj-foundation/src/main/java/com/webforj/component/window/Window.java
index 5fb1e2fa..71db67cd 100644
--- a/webforj-foundation/src/main/java/com/webforj/component/window/Window.java
+++ b/webforj-foundation/src/main/java/com/webforj/component/window/Window.java
@@ -1,6 +1,7 @@
 package com.webforj.component.window;
 
 import com.basis.bbj.proxies.sysgui.BBjWindow;
+import com.basis.startup.type.BBjException;
 import com.webforj.bridge.ComponentAccessor;
 import com.webforj.bridge.WindowAccessor;
 import com.webforj.component.Component;
@@ -93,6 +94,19 @@ public abstract class Window extends DwcContainer<Window> {
     // pass
   }
 
+  @Override
+  protected void onDestroy() {
+    super.onDestroy();
+
+    try {
+      getBbjWindow().destroy();
+    } catch (BBjException e) {
+      // pass
+    } finally {
+      setBbjWindow(null);
+    }
+  }
+
   /**
    * {@inheritDoc}
    */
diff --git a/webforj-foundation/src/main/java/com/webforj/environment/ObjectTable.java b/webforj-foundation/src/main/java/com/webforj/environment/ObjectTable.java
index fdfc1cb2..9ae5eb85 100644
--- a/webforj-foundation/src/main/java/com/webforj/environment/ObjectTable.java
+++ b/webforj-foundation/src/main/java/com/webforj/environment/ObjectTable.java
@@ -1,8 +1,8 @@
 package com.webforj.environment;
 
 import com.basis.startup.type.BBjException;
-import java.util.NoSuchElementException;
 import com.webforj.Environment;
+import java.util.NoSuchElementException;
 
 /**
  * Provides access to a map of key value pairs that is accessible at all program levels.
@@ -21,9 +21,12 @@ public final class ObjectTable {
    *
    * @param key the key of the variable to access
    * @param value the contents to set in the field
+   *
+   * @return the value that was set
    */
-  public static void put(String key, Object value) {
-    Environment.getCurrent().getBBjAPI().getObjectTable().put(key, value);
+  public static Object put(String key, Object value) {
+    Environment.ifPresent(env -> env.getBBjAPI().getObjectTable().put(key, value));
+    return value;
   }
 
   /**
@@ -35,7 +38,12 @@ public final class ObjectTable {
    */
   public static Object get(String key) {
     try {
-      return Environment.getCurrent().getBBjAPI().getObjectTable().get(key);
+      Environment env = Environment.getCurrent();
+      if (env == null) {
+        throw new NoSuchElementException("Element " + key + " does not exist!");
+      }
+
+      return env.getBBjAPI().getObjectTable().get(key);
     } catch (BBjException e) {
       throw new NoSuchElementException("Element " + key + " does not exist!");
     }
@@ -62,7 +70,7 @@ public final class ObjectTable {
    * @param key the key of the variable to remove
    */
   public static void clear(String key) {
-    Environment.getCurrent().getBBjAPI().getObjectTable().remove(key);
+    Environment.ifPresent(env -> env.getBBjAPI().getObjectTable().remove(key));
   }
 
   /**
@@ -71,6 +79,11 @@ public final class ObjectTable {
    * @return the number of key-value mappings in this map
    */
   public static int size() {
-    return Environment.getCurrent().getBBjAPI().getObjectTable().size();
+    Environment env = Environment.getCurrent();
+    if (env == null) {
+      return 0;
+    }
+
+    return env.getBBjAPI().getObjectTable().size();
   }
 }
diff --git a/webforj-foundation/src/main/java/com/webforj/environment/StringTable.java b/webforj-foundation/src/main/java/com/webforj/environment/StringTable.java
index 78868e52..7f9fa84b 100644
--- a/webforj-foundation/src/main/java/com/webforj/environment/StringTable.java
+++ b/webforj-foundation/src/main/java/com/webforj/environment/StringTable.java
@@ -1,8 +1,8 @@
 package com.webforj.environment;
 
 import com.basis.startup.type.BBjException;
-import java.util.NoSuchElementException;
 import com.webforj.Environment;
+import java.util.NoSuchElementException;
 
 /**
  * Provides access to the STBL String Table.
@@ -54,7 +54,12 @@ public final class StringTable {
    */
   public static String get(String key) {
     try {
-      return Environment.getCurrent().getBBjAPI().getStbl(key);
+      Environment env = Environment.getCurrent();
+      if (env == null) {
+        throw new NoSuchElementException("Element " + key + " does not exist!");
+      }
+
+      return env.getBBjAPI().getStbl(key);
     } catch (BBjException e) {
       throw new NoSuchElementException("Element " + key + " does not exist!");
     }
@@ -66,14 +71,17 @@ public final class StringTable {
    * @param key the key of the variable to access
    * @param value the contents to set in the field
    *
-   * @return the value just set
+   * @return the value that was set
    */
   public static String put(String key, String value) {
-    try {
-      Environment.getCurrent().getBBjAPI().setStbl(key, value);
-    } catch (BBjException e) {
-      Environment.logError(e);
-    }
+    Environment.ifPresent(env -> {
+      try {
+        env.getBBjAPI().setStbl(key, value);
+      } catch (BBjException e) {
+        // pass
+      }
+    });
+
     return value;
   }
 
@@ -83,10 +91,12 @@ public final class StringTable {
    * @param key the key of the variable to remove
    */
   public static void clear(String key) {
-    try {
-      Environment.getCurrent().getBBjAPI().setStbl("!CLEAR", key);
-    } catch (BBjException e) {
-      Environment.logError(e);
-    }
+    Environment.ifPresent(env -> {
+      try {
+        env.getBBjAPI().setStbl("!CLEAR", key);
+      } catch (BBjException e) {
+        // pass
+      }
+    });
   }
 }
diff --git a/webforj-foundation/src/main/java/com/webforj/error/ErrorHandler.java b/webforj-foundation/src/main/java/com/webforj/error/ErrorHandler.java
index 0158367a..cc677a02 100644
--- a/webforj-foundation/src/main/java/com/webforj/error/ErrorHandler.java
+++ b/webforj-foundation/src/main/java/com/webforj/error/ErrorHandler.java
@@ -4,7 +4,6 @@ import com.webforj.NoneAction;
 import com.webforj.Page;
 import com.webforj.exceptions.WebforjWebManagerException;
 import java.util.Base64;
-import java.util.logging.Logger;
 
 /**
  * The {@code ErrorHandler} interface is designed to handle errors that occur during the execution
@@ -50,17 +49,17 @@ public interface ErrorHandler {
    * @param content The content of the error page.
    */
   default void showErrorPage(String title, String content) {
-    try {
-      Base64.Encoder encoder = Base64.getEncoder();
-      String encodedHtml = encoder.encodeToString(content.getBytes());
+    Page.ifPresent(page -> {
+      try {
+        Base64.Encoder encoder = Base64.getEncoder();
+        String encodedHtml = encoder.encodeToString(content.getBytes());
 
-      String js = "document.body.innerHTML = atob('" + encodedHtml + "');";
-      Page.getCurrent().executeJs(js);
-      Page.getCurrent().setTitle(title);
-    } catch (WebforjWebManagerException ex) {
-      final Logger log = Logger.getLogger("com.webforj.error.ErrorHandler");
-      log.severe(
-          "Failed to display the error page in the browser. Web Manager API are not available.");
-    }
+        String js = "document.body.innerHTML = atob('" + encodedHtml + "');";
+        page.executeJs(js);
+        page.setTitle(title);
+      } catch (WebforjWebManagerException ex) {
+        // pass
+      }
+    });
   }
 }
diff --git a/webforj-foundation/src/main/java/com/webforj/error/GlobalErrorHandler.java b/webforj-foundation/src/main/java/com/webforj/error/GlobalErrorHandler.java
index 63d4c832..ca94d106 100644
--- a/webforj-foundation/src/main/java/com/webforj/error/GlobalErrorHandler.java
+++ b/webforj-foundation/src/main/java/com/webforj/error/GlobalErrorHandler.java
@@ -2,7 +2,7 @@ package com.webforj.error;
 
 import static com.webforj.App.console;
 
-import java.util.logging.Logger;
+import java.lang.System.Logger;
 
 /**
  * The {@code GlobalErrorHandler} class is used to handle errors that occur during the execution of
@@ -18,15 +18,14 @@ import java.util.logging.Logger;
  * @since 24.12
  */
 public class GlobalErrorHandler implements ErrorHandler {
-  private static final Logger log = Logger.getLogger("com.webforj.error.GlobalErrorHandler");
+  private static final Logger logger = System.getLogger(GlobalErrorHandler.class.getName());
 
   /**
    * {@inheritDoc}
    */
   @Override
   public void onError(Throwable throwable, boolean debug) {
-    // Log to BBj Debug log
-    log.severe(throwable.getMessage());
+    logger.log(Logger.Level.ERROR, throwable.getMessage(), throwable);
 
     // log to the browser console (debug only)
     console().error(throwable);
diff --git a/webforj-foundation/src/main/java/com/webforj/event/page/PageUnloadEventHandler.java b/webforj-foundation/src/main/java/com/webforj/event/page/PageUnloadEventHandler.java
index 2e52885b..4813551f 100644
--- a/webforj-foundation/src/main/java/com/webforj/event/page/PageUnloadEventHandler.java
+++ b/webforj-foundation/src/main/java/com/webforj/event/page/PageUnloadEventHandler.java
@@ -32,5 +32,7 @@ public final class PageUnloadEventHandler {
    */
   public void handleEvent(BBjEvent ev) {
     dispatcher.dispatchEvent(new PageUnloadEvent(page));
+    // cleanup everything related to the page
+    dispatcher.removeAllListeners();
   }
 }
diff --git a/webforj-foundation/src/main/java/com/webforj/exceptions/WebforjWebManagerException.java b/webforj-foundation/src/main/java/com/webforj/exceptions/WebforjWebManagerException.java
index ac2d5525..23b83ff9 100644
--- a/webforj-foundation/src/main/java/com/webforj/exceptions/WebforjWebManagerException.java
+++ b/webforj-foundation/src/main/java/com/webforj/exceptions/WebforjWebManagerException.java
@@ -7,7 +7,7 @@ package com.webforj.exceptions;
  * @author Hyyan Abo Fakher
  * @since 24.20
  */
-public class WebforjWebManagerException extends RuntimeException {
+public class WebforjWebManagerException extends WebforjRuntimeException {
 
   /**
    * Constructs a new exception with the specified detail message. The cause is not initialized, and
diff --git a/webforj-foundation/src/main/java/com/webforj/router/Router.java b/webforj-foundation/src/main/java/com/webforj/router/Router.java
index 398dfa2a..6099a5c1 100644
--- a/webforj-foundation/src/main/java/com/webforj/router/Router.java
+++ b/webforj-foundation/src/main/java/com/webforj/router/Router.java
@@ -615,6 +615,15 @@ public class Router {
     return addNavigateListener(listener);
   }
 
+  /**
+   * Remove All listeners.
+   */
+  public void removeAllListeners() {
+    removeHistoryStateListener();
+    getEventDispatcher().removeAllListeners();
+    getHistory().removeAllListeners();
+  }
+
   /**
    * Retrieves the event dispatcher.
    *
diff --git a/webforj-foundation/src/main/java/com/webforj/router/history/BrowserHistory.java b/webforj-foundation/src/main/java/com/webforj/router/history/BrowserHistory.java
index 0f414652..55d85baf 100644
--- a/webforj-foundation/src/main/java/com/webforj/router/history/BrowserHistory.java
+++ b/webforj-foundation/src/main/java/com/webforj/router/history/BrowserHistory.java
@@ -6,6 +6,7 @@ import com.webforj.dispatcher.EventDispatcher;
 import com.webforj.dispatcher.EventListener;
 import com.webforj.dispatcher.ListenerRegistration;
 import com.webforj.event.page.PageEventOptions;
+import com.webforj.exceptions.WebforjRuntimeException;
 import com.webforj.router.history.event.HistoryStateChangeEvent;
 import java.util.Base64;
 import java.util.Optional;
@@ -28,7 +29,7 @@ public class BrowserHistory implements History {
    */
   @Override
   public History back() {
-    Page.getCurrent().executeJsVoidAsync("window.history.back()");
+    Page.ifPresent(page -> page.executeJsVoidAsync("window.history.back()"));
     return this;
   }
 
@@ -37,7 +38,7 @@ public class BrowserHistory implements History {
    */
   @Override
   public History forward() {
-    Page.getCurrent().executeJsVoidAsync("window.history.forward()");
+    Page.ifPresent(page -> page.executeJsVoidAsync("window.history.forward()"));
     return this;
   }
 
@@ -46,7 +47,7 @@ public class BrowserHistory implements History {
    */
   @Override
   public History go(int index) {
-    Page.getCurrent().executeJsVoidAsync("window.history.go(" + index + ")");
+    Page.ifPresent(page -> page.executeJsVoidAsync("window.history.go(" + index + ")"));
     return this;
   }
 
@@ -55,6 +56,10 @@ public class BrowserHistory implements History {
    */
   @Override
   public int size() {
+    if (!Page.isPresent()) {
+      return 0;
+    }
+
     Object result = Page.getCurrent().executeJs("window.history.length");
     if (result instanceof Integer) {
       return (int) result;
@@ -68,6 +73,10 @@ public class BrowserHistory implements History {
    */
   @Override
   public Optional<Location> getLocation() {
+    if (!Page.isPresent()) {
+      return Optional.empty();
+    }
+
     Object result =
         Page.getCurrent().executeJs("window.location.href.replace(window.location.origin, '')");
     if (result instanceof String stringResult) {
@@ -82,12 +91,14 @@ public class BrowserHistory implements History {
    */
   @Override
   public History pushState(Object state, Location location) {
-    String path = location.getFullURI();
-    String stringifiedState = gson.toJson(state == null ? new Object() : state);
-    String encodedState = Base64.getEncoder().encodeToString(stringifiedState.getBytes());
+    Page.ifPresent(page -> {
+      String path = location.getFullURI();
+      String stringifiedState = gson.toJson(state == null ? new Object() : state);
+      String encodedState = Base64.getEncoder().encodeToString(stringifiedState.getBytes());
 
-    Page.getCurrent().executeJsVoidAsync(
-        "window.history.pushState(JSON.parse(atob('" + encodedState + "')), '', '" + path + "')");
+      page.executeJsVoidAsync(
+          "window.history.pushState(JSON.parse(atob('" + encodedState + "')), '', '" + path + "')");
+    });
 
     return this;
   }
@@ -97,12 +108,14 @@ public class BrowserHistory implements History {
    */
   @Override
   public History replaceState(Object state, Location location) {
-    String path = location.getFullURI();
-    String stringifiedState = gson.toJson(state == null ? new Object() : state);
-    String encodedState = Base64.getEncoder().encodeToString(stringifiedState.getBytes());
+    Page.ifPresent(page -> {
+      String path = location.getFullURI();
+      String stringifiedState = gson.toJson(state == null ? new Object() : state);
+      String encodedState = Base64.getEncoder().encodeToString(stringifiedState.getBytes());
 
-    Page.getCurrent().executeJsVoidAsync("window.history.replaceState(JSON.parse(atob('"
-        + encodedState + "')), '', '" + path + "')");
+      page.executeJsVoidAsync("window.history.replaceState(JSON.parse(atob('" + encodedState
+          + "')), '', '" + path + "')");
+    });
 
     return this;
   }
@@ -112,6 +125,10 @@ public class BrowserHistory implements History {
    */
   @Override
   public <T> Optional<T> getState(Class<T> classOfT) {
+    if (!Page.isPresent()) {
+      return Optional.empty();
+    }
+
     Object result = Page.getCurrent().executeJs("btoa(JSON.stringify(window.history.state))");
 
     if (result != null) {
@@ -126,6 +143,15 @@ public class BrowserHistory implements History {
     return Optional.empty();
   }
 
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public History removeAllListeners() {
+    dispatcher.removeAllListeners();
+    return this;
+  }
+
   /**
    * {@inheritDoc}
    */
@@ -141,23 +167,32 @@ public class BrowserHistory implements History {
   }
 
   private void registerPopStateListener() {
-    PageEventOptions options = new PageEventOptions();
-    options.addData("state", "btoa(JSON.stringify(event.state))");
-    options.addData("path", "window.location.href.replace(window.location.origin, '')");
+    Page.ifPresent(page -> { // NOSONAR
+      PageEventOptions options = new PageEventOptions();
+      options.addData("state", "btoa(JSON.stringify(event.state))");
+      options.addData("path", "window.location.href.replace(window.location.origin, '')");
 
-    Page.getCurrent().addEventListener("popstate", ev -> {
-      String state = (String) ev.getData().get("state");
-      String path = (String) ev.getData().get("path");
-
-      Object decodedState = null;
-      if (state != null) {
-        decodedState = gson.fromJson(new String(Base64.getDecoder().decode(state)), Object.class);
+      try {
+        Page.getCurrent().addEventListener("popstate", ev -> {
+          String state = (String) ev.getData().get("state");
+          String path = (String) ev.getData().get("path");
+
+          Object decodedState = null;
+          if (state != null) {
+            decodedState =
+                gson.fromJson(new String(Base64.getDecoder().decode(state)), Object.class);
+          }
+
+          HistoryStateChangeEvent event =
+              new HistoryStateChangeEvent(this, new Location(path), decodedState);
+
+          dispatcher.dispatchEvent(event);
+        }, options);
+      } catch (WebforjRuntimeException e) {
+        // pass
+        // Technically, this could happen with a headless browser, but we don't want to throw an
+        // exception in that case.
       }
-
-      HistoryStateChangeEvent event =
-          new HistoryStateChangeEvent(this, new Location(path), decodedState);
-
-      dispatcher.dispatchEvent(event);
-    }, options);
+    });
   }
 }
diff --git a/webforj-foundation/src/main/java/com/webforj/router/history/History.java b/webforj-foundation/src/main/java/com/webforj/router/history/History.java
index 388e2ba4..50aa98b3 100644
--- a/webforj-foundation/src/main/java/com/webforj/router/history/History.java
+++ b/webforj-foundation/src/main/java/com/webforj/router/history/History.java
@@ -108,6 +108,11 @@ public interface History {
     return getState(Object.class);
   }
 
+  /**
+   * Remove all registered listeners.
+   */
+  History removeAllListeners();
+
   /**
    * Adds a listener to be notified when the history state changes.
    *
diff --git a/webforj-foundation/src/main/java/com/webforj/router/history/MemoryHistory.java b/webforj-foundation/src/main/java/com/webforj/router/history/MemoryHistory.java
index 5d8d0933..5a449f64 100644
--- a/webforj-foundation/src/main/java/com/webforj/router/history/MemoryHistory.java
+++ b/webforj-foundation/src/main/java/com/webforj/router/history/MemoryHistory.java
@@ -149,6 +149,15 @@ public class MemoryHistory implements History {
     return Optional.empty();
   }
 
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public History removeAllListeners() {
+    dispatcher.removeAllListeners();
+    return this;
+  }
+
   /**
    * {@inheritDoc}
    */
diff --git a/webforj-foundation/src/main/resources/bbj/webforj.bbj b/webforj-foundation/src/main/resources/bbj/webforj.bbj
index 8645fffb..42cf7ee6 100644
--- a/webforj-foundation/src/main/resources/bbj/webforj.bbj
+++ b/webforj-foundation/src/main/resources/bbj/webforj.bbj
@@ -8,7 +8,7 @@ use ::admin.bbj::UserCredentials
 use ::!!bbj/WebforjHelper.bbj::WebforjHelper
 
 SETESC EscTrap
-SETERR _error
+SETERR ErrTrap
 
 BBJAPI().getConfig().releaseOnLostConnection(0)
 BBjAPI().setCustomEventCallback("webforjTerminateSignal","terminate")
@@ -55,12 +55,8 @@ web!.setEndAction(noneAction!)
 web!.setEndAction(noneAction!)
 
 REM launch the app
-Bootstrap.init(BBjAPI(), new WebforjHelper(), DEBUG, class$)
-process_events,err=_error
-
-terminate:
-  Environment.cleanup()
-release
+app! = Bootstrap.init(BBjAPI(), new WebforjHelper(), DEBUG, class$)
+process_events,err=ErrTrap
 
 determineClasspathEntries:
   pw$="admin123"
@@ -108,25 +104,25 @@ onHotReload:
   release
 return
 
-EscTrap:
-  REM IF AND(CHR(TCB(19)),$08$)=$08$ THEN
-  REM     System.out.println("Client Died")
-  REM ELSE
-  REM     System.out.println("ESCAPE")
-  REM FI
+terminate:
+  Environment.cleanup()
   if app! <> null() then
-      app!.cleanup()
+      app! = null()
+      app!.terminate()
   fi
 release
 
-ErrTrap:
-  System.out.println("Error Caught: "+str(err)+" "+errmes(-1))
-  if app! <> null() then
-      app!.cleanup()
-  fi
+EscTrap:
+  IF AND(CHR(TCB(19)),$08$)=$08$ THEN
+      System.out.println("Client connection lost. Terminating.")
+  ELSE
+      System.out.println("An ESCAPE was requested. Terminating.")
+  FI
+
+  gosub terminate
 release
 
-_error:
+ErrTrap:
   lastJavaException! = bbjapi().getLastJavaException()
   if lastJavaException! <> null() then
     debugMode = DEBUG
@@ -135,7 +131,8 @@ _error:
     fi
     Environment.handleError(lastJavaException!, debugMode)
   else
-    System.err.println("Fatal Error: ")
-    System.err.println(lastJavaException!)
+    System.err.println("Fatal Error with no Java Exception caught.")
   fi
+
+  gosub terminate
 release


Test patch:
diff --git a/webforj-data/src/test/java/com/webforj/data/EntityKeysRegistryTest.java b/webforj-data/src/test/java/com/webforj/data/EntityKeysRegistryTest.java
index fee5376c..728d339b 100644
--- a/webforj-data/src/test/java/com/webforj/data/EntityKeysRegistryTest.java
+++ b/webforj-data/src/test/java/com/webforj/data/EntityKeysRegistryTest.java
@@ -70,4 +70,19 @@ class EntityKeysRegistryTest {
   void shouldThrowNullPointerExceptionWhenEntityIsNullInRefreshKey() {
     assertThrows(NullPointerException.class, () -> registry.refreshKey(null));
   }
+
+  @Test
+  void shouldRemoveEntityAndItsKey() {
+    registry.removeEntity(testEntity);
+    assertNull(registry.getEntity(testEntityKey));
+  }
+
+  @Test
+  void shouldCleanUpStaleEntries() {
+    Object entity2 = new Object();
+    String key2 = registry.getKey(entity2);
+    registry.removeEntity(entity2); // Simulate stale entry
+    registry.cleanUp();
+    assertNull(registry.getEntity(key2));
+  }
 }
diff --git a/webforj-foundation/src/test/java/com/webforj/component/SlotRegistryTest.java b/webforj-foundation/src/test/java/com/webforj/component/SlotRegistryTest.java
index fc29cf37..830a276b 100644
--- a/webforj-foundation/src/test/java/com/webforj/component/SlotRegistryTest.java
+++ b/webforj-foundation/src/test/java/com/webforj/component/SlotRegistryTest.java
@@ -170,6 +170,21 @@ class SlotRegistryTest {
     assertTrue(slots.contains("footer"));
   }
 
+  @Test
+  void shouldDisposeAllComponentsAndClearSlots() {
+    slotRegistry.addComponentsToSlot("header", component1);
+    slotRegistry.addComponentsToSlot("footer", component2);
+
+    slotRegistry.dispose();
+
+    assertTrue(slotRegistry.getComponentsInSlot("header").isEmpty());
+    assertTrue(slotRegistry.getComponentsInSlot("footer").isEmpty());
+    assertTrue(slotRegistry.getSlots().isEmpty());
+
+    verify(component1).destroy();
+    verify(component2).destroy();
+  }
+
   static class TestComponent extends Component {
 
     @Override
diff --git a/webforj-foundation/src/test/java/com/webforj/environment/ObjectTableTest.java b/webforj-foundation/src/test/java/com/webforj/environment/ObjectTableTest.java
new file mode 100644
index 00000000..13e84ac6
--- /dev/null
+++ b/webforj-foundation/src/test/java/com/webforj/environment/ObjectTableTest.java
@@ -0,0 +1,87 @@
+package com.webforj.environment;
+
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.basis.bbj.proxies.BBjAPI;
+import com.basis.bbj.proxies.BBjObjectTable;
+import com.basis.startup.type.BBjException;
+import com.webforj.Environment;
+import com.webforj.bridge.WebforjBBjBridge;
+import java.util.NoSuchElementException;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class ObjectTableTest {
+  BBjObjectTable mockObjectTable;
+
+  private final String testKey = "testKey";
+  private final Object testValue = "testValue";
+  private final String nonExistentKey = "nonExistentKey";
+
+  @BeforeEach
+  void setUp() throws BBjException {
+    BBjAPI mockApi = mock(BBjAPI.class);
+    mockObjectTable = mock(BBjObjectTable.class);
+    when(mockApi.getObjectTable()).thenReturn(mockObjectTable);
+    Environment.init(mockApi, mock(WebforjBBjBridge.class), 0);
+  }
+
+  @AfterEach
+  void tearDown() {
+    Environment.cleanup();
+  }
+
+  @Test
+  void shouldPutValue() {
+    ObjectTable.put(testKey, testValue);
+    verify(mockObjectTable).put(testKey, testValue);
+  }
+
+  @Test
+  void shouldGetValue() throws BBjException {
+    when(mockObjectTable.get(testKey)).thenReturn(testValue);
+    Object actualValue = ObjectTable.get(testKey);
+    assertEquals(testValue, actualValue);
+  }
+
+  @Test
+  void shouldThrowExceptionWhenKeyNotFound() throws BBjException {
+    when(mockObjectTable.get(nonExistentKey)).thenThrow(new NoSuchElementException());
+    assertThrows(NoSuchElementException.class, () -> ObjectTable.get(nonExistentKey));
+  }
+
+  @Test
+  void shouldReturnTrueIfKeyExists() throws BBjException {
+    when(mockObjectTable.get(testKey)).thenReturn(testValue);
+    assertTrue(ObjectTable.contains(testKey));
+  }
+
+  @Test
+  void shouldReturnFalseIfKeyNotFound() throws BBjException {
+    when(mockObjectTable.get(nonExistentKey)).thenThrow(new NoSuchElementException());
+    assertFalse(ObjectTable.contains(nonExistentKey));
+  }
+
+  @Test
+  void shouldClearValueFromObjectTable() {
+    ObjectTable.clear(testKey);
+    verify(mockObjectTable).remove(testKey);
+  }
+
+  @Test
+  void shouldReturnSizeOfObjectTable() {
+    int expectedSize = 5;
+    when(mockObjectTable.size()).thenReturn(expectedSize);
+
+    int actualSize = ObjectTable.size();
+    assertEquals(expectedSize, actualSize);
+  }
+}
diff --git a/webforj-foundation/src/test/java/com/webforj/environment/StringTableTest.java b/webforj-foundation/src/test/java/com/webforj/environment/StringTableTest.java
new file mode 100644
index 00000000..3d5604f7
--- /dev/null
+++ b/webforj-foundation/src/test/java/com/webforj/environment/StringTableTest.java
@@ -0,0 +1,68 @@
+package com.webforj.environment;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.basis.bbj.proxies.BBjAPI;
+import com.basis.startup.type.BBjException;
+import com.webforj.Environment;
+import com.webforj.bridge.WebforjBBjBridge;
+import java.util.NoSuchElementException;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class StringTableTest {
+  BBjAPI mockApi;
+
+  private final String testKey = "testKey";
+  private final String testValue = "testValue";
+  private final String nonExistentKey = "nonExistentKey";
+
+  @BeforeEach
+  void setUp() throws BBjException {
+    mockApi = mock(BBjAPI.class);
+    when(mockApi.getStbl(testKey)).thenReturn(testValue);
+    Environment.init(mockApi, mock(WebforjBBjBridge.class), 0);
+  }
+
+  @AfterEach
+  void tearDown() {
+    Environment.cleanup();
+  }
+
+  @Test
+  void shouldPutValueInStringTable() {
+    StringTable.put(testKey, testValue);
+    assertTrue(StringTable.contains(testKey));
+  }
+
+  @Test
+  void shouldGetValueFromStringTable() throws BBjException {
+    when(mockApi.getStbl(testKey)).thenReturn(testValue);
+    String actualValue = StringTable.get(testKey);
+    assertEquals(testValue, actualValue);
+  }
+
+  @Test
+  void shouldThrowExceptionWhenKeyNotFound() throws BBjException {
+    when(mockApi.getStbl(nonExistentKey)).thenThrow(new NoSuchElementException());
+    assertThrows(NoSuchElementException.class, () -> StringTable.get(nonExistentKey));
+  }
+
+  @Test
+  void shouldReturnTrueIfKeyExists() throws BBjException {
+    when(mockApi.getStbl(testKey)).thenReturn(testValue);
+    assertTrue(StringTable.contains(testKey));
+  }
+
+  @Test
+  void shouldReturnFalseIfKeyNotFound() throws BBjException {
+    when(mockApi.getStbl(nonExistentKey)).thenThrow(new NoSuchElementException());
+    assertFalse(StringTable.contains(nonExistentKey));
+  }
+}