Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

General Exception handling in JavaFX 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?

like image 540
Hannes Avatar asked Oct 14 '14 13:10

Hannes


People also ask

What are the types of exception handling in Java?

There are mainly two types of exceptions in Java as follows: Checked exception. Unchecked exception.

What is exception handling in Java?

Java Exception Handling is a mechanism to handle runtime errors such as ClassNotFoundException, IOException, SQLException, RemoteException, etc. Exception is an unwanted or unexpected event, which occurs during the execution of a program, i.e. at run time, that disrupts the normal flow of the program's instructions.


2 Answers

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();
    }
}
like image 152
James_D Avatar answered Oct 16 '22 18:10

James_D


This is actually kind of tricky, I encountered the same issue before, and I couldn't come up with any elegant solutions. Obviously, one really heavy-handed way (and honestly, probably totally wrong way) to handle this, is in each of the controller class methods (the ones that begin with @FXML), wrap the whole body of the method in a try{} catch(Throwable t){} block, and then inside of your throwable catch, do some analysis on the result of the exception to try and determine what useful information to show the user in the event of disaster.

It's also worth noting, that at least in Javafx 8 (I haven't tried with 2.0-2.2) if you try wrapping the place where you load the FXML (like in your applications main 'Start' method, for example), in the same kind of throwable block, it Does not catch the exception from the Controller class Which seems to imply some sort of separation between that thread and the one being used in the FXML Controller class. However, It is definitely on the same Application thread, since if you keep a reference to the Thread.currentThread(); object in the calling class, and then do the same in the controller, the .equals on the two will turn out true. So under the sheets Javafx is doing some magic to detach unchecked exceptions from those classes.

I haven't looked any further into it than that.

Truth be told, I hate even having this answer up here, because I'm afraid someone will use it without the proper understanding of just how incorrect this is. As such, if someone pipes in with a better answer, I'm going to delete this straightaway.

Good luck!

like image 41
WillBD Avatar answered Oct 16 '22 20:10

WillBD