Java – General Exception handling in JavaFX 8

exception handlingjavajava-8javafxjavafx-8

Given the controller of a Scene calls business code which raises an Exception. How can I handle those kind of Exceptions in a general fashion?

I tried the Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler) method but it is not invoked so I believe that the Exceptions are catched somewhere inside the JavaFX framework.

What could I do to handle this Exceptions or at least show some useful information to the user?

Best Answer

As of JavaFX 8, Thread.setDefaultUncaughtExceptionHandler(...) should work: see RT-15332.

Things are a little complicated if an uncaught exception occurs during execution of the start(...) method. Depending on how the application is launched, the code that invokes start() (e.g. the implementation of Application.launch(...)) may catch the exception and handle it, which would obviously prevent the default exception handler from being invoked.

In particular, on my system (JDK 1.8.0_20 on Mac OS X 10.9.5), it appears that if my application starts up via a main(...) method that invokes Application.launch(...), any exception thrown in the start() method is caught (and not rethrown).

However, if I remove the main(...) method (see note below) and launch the application directly, any exception thrown in the start() method is rethrown, allowing the default exception handler to be invoked. Note that it doesn't merely propagate up. start() is invoked on the FX Application Thread and the exception is rethrown from the main thread. Indeed, when this occurs, code in the default handler that assumes the FX Application Thread is running fails to run: so my guess is that the launching code in this case catches exceptions in the start() method, and in the catch block, shuts down the FX Application Thread, and then rethrows the exception from the calling thread.

The upshot of all this is that it is important - if you want your default handler to handle exceptions in the start() method, you should not call any UI code if the exception is not thrown on the FX Application Thread (even via a Platform.runLater(...)).

Note: (for those who may not be aware of this). As of Java 8, you can directly launch an Application subclass even if it doesn't have a main(...) method, by passing the classname as an argument to the JVM executable in the usual way (i.e. java MyApp). This does what you'd expect: starts up the FX toolkit, starts the FX Application thread, instantiates the Application subclass and calls init(), then on the FX Application Thread calls start(). Interestingly (and perhaps incorrectly), a main(...) method that invokes Application.launch() behaves slightly differently with respect to uncaught exceptions in the start(...) method.

Here is a basic example. Uncomment the code in Controller.initialize() to see an exception thrown in the start() method.

package application;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage;


public class Main extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {

        Thread.setDefaultUncaughtExceptionHandler(Main::showError);

        Parent root = FXMLLoader.load(getClass().getResource("Main.fxml"));
        Scene scene = new Scene(root,400,400);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private static void showError(Thread t, Throwable e) {
        System.err.println("***Default exception handler***");
        if (Platform.isFxApplicationThread()) {
            showErrorDialog(e);
        } else {
            System.err.println("An unexpected error occurred in "+t);

        }
    }

    private static void showErrorDialog(Throwable e) {
        StringWriter errorMsg = new StringWriter();
        e.printStackTrace(new PrintWriter(errorMsg));
        Stage dialog = new Stage();
        dialog.initModality(Modality.APPLICATION_MODAL);
        FXMLLoader loader = new FXMLLoader(Main.class.getResource("Error.fxml"));
        try {
            Parent root = loader.load();
            ((ErrorController)loader.getController()).setErrorText(errorMsg.toString());
            dialog.setScene(new Scene(root, 250, 400));
            dialog.show();
        } catch (IOException exc) {
            exc.printStackTrace();
        }
    }

//  public static void main(String[] args) {
//      launch(args);
//  }
}

With Main.fxml:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.geometry.Insets?>

<HBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.Controller"
    alignment="center" spacing="5">
    <children>
        <Button text="Do something safe" onAction="#safeHandler" />
        <Button text="Do something risky" onAction="#riskyHandler" />
        <Label fx:id="label" />
    </children>
    <padding>
        <Insets top="10" left="10" right="10" bottom="10" />
    </padding>
</HBox>

Controller.java:

package application;

import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Label;

public class Controller {
    private final IntegerProperty counter = new SimpleIntegerProperty(1);

    @FXML
    private Label label ;

    public void initialize() throws Exception {
        label.textProperty().bind(Bindings.format("Count: %s", counter));

        // uncomment the next line to demo exceptions in the start() method:
        // throw new Exception("Initializer exception");
    }

    @FXML
    private void safeHandler() {
        counter.set(counter.get()+1);
    }

    @FXML
    private void riskyHandler() throws Exception {
        if (Math.random() < 0.5) {
            throw new RuntimeException("An unknown error occurred");
        }
        safeHandler();
    }
}

Error.fxml:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.Button?>

<BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.ErrorController">
    <center>
        <ScrollPane>
            <content>
                <Label fx:id="errorMessage" wrapText="true" />
            </content>
        </ScrollPane>
    </center>
    <bottom>
        <HBox alignment="CENTER">
            <Button text="OK" onAction="#close"/>
        </HBox>
    </bottom>
</BorderPane>

ErrorController.java:

package application;

import javafx.fxml.FXML;
import javafx.scene.control.Label;

public class ErrorController {
    @FXML
    private Label errorMessage ;

    public void setErrorText(String text) {
        errorMessage.setText(text);
    }

    @FXML
    private void close() {
        errorMessage.getScene().getWindow().hide();
    }
}