Reading Stacktraces

Jul 19, 2015
Reading Stacktraces
  • Reading Stacktraces

    How to easily fix plugin problems



    What's a stacktrace?
    [​IMG]
    (Yes, this stacktrace is old. I fetched it off google...)
    This is an example for a stacktrace. A stacktrace is printed to the console whenever there's an exception that's un-handled. For example, the following code will throw an IllegalArgumentException. This exception is thrown when an argument is accessed when there is no argument in the array.
    Code (Text):
    String[] array = {"argument 0", "argument 1", null};
    //This part is okay since there is an array[0] => "argument 0"
    String str = array[0];
    //This part is also okay since there is an argument[2] => null
    String str = array[2];
    //This part will throw an exception since there is no argument[3].
    String st = array[3];
    Once an exception is thrown, the code will not continue to run.
    But whenever a stacktrace shows up in the console the code keeps running! Why doesn't the server shut down whenever there's an exception?
    That's because bukkit's smarter than that. Bukkit catches and handles every exception and continues normal execution of the code.


    What does that error message mean?
    Well usually most of the stacktrace shouldn't mean anything to you, because it points on classes that are not yours aswell as classes that you did create.
    Now, if we look closer at the stacktrace, we can see some important details:
    [​IMG]
    1. The part highlighted in blue is the exception - as we can see, in this case it's a NullPointerException, an exception that is thrown when an object that is null is accessed.
    2. The part highlighted in yellow is the last call, where the error first occurs. We can see in what package (me.Zach_1919.xpjar), The class (Main), the method (onInteract), and the line (Main.java:40 (line 40)). Whatever's below that are the calls from last to first (more details about that below!)
    3. The part in red is the plugin name and version, so you know in which plugin the exception is thrown.
    4. The other stuff between the red part and the blue part are internal bukkit calls, meaning they're called inside bukkit and aren't relevant.

    Reading stacktraces even more efficiently (Thank you @Rocoty for this)
    Above we saw how to handle the exception properly. But what happens if the problem is not in the last call (where the error firstly occurs, the yellow part)?
    Let's get some examples. Consider the following code: (this is plain simple Java code)
    Code (Text):
    public class Test {

        public static void main(String[] args) {
            printLength(null); //line 4 //We are deliberately generating a NullPointerException
        }

        public static void printLength(String s) {
            System.out.println(s.length()); //line 8
        }

    }
     
    If we run this code, we get a NullPointerException with a StackTrace looking something like this:
    Code (Text):
    Exception in thread "main" java.lang.NullPointerException
        at com.gmail.oksandum.test.Test.printLength(Test.java:8)
        at com.gmail.oksandum.test.Test.main(Test.java:4)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:483)
    If we study this, we will see that the first line in the stacktrace (below java.lang.NullPointerException) does in fact point directly to where the error first occurred, namely the printLength method. Remember, we passed a null String to the method, so trying to dereference it (read: call a method on it) will certainly produce a NullPointerException. Also notice that the second line is pointing to the main method. Interestingly, this is the method that called the printLength method. Do you now see what I mean when I say they are printed in order from last to first call?

    But why is all this detail so important to know? Can't I just check all the places in my own code that show up in the stacktrace? Well...yes. Yes you could. But would you really want to risk all that precious time and headache? Knowing how exceptions and stacktraces behave can really speed up the debugging process so you can focus on the more important thing in the code.
    Here's an example where this could come in handy (this code is un-realistic, but bear with me):
    Code (Text):
    import org.bukkit.configuration.file.FileConfiguration;
    import org.bukkit.plugin.java.JavaPlugin;

    import java.util.logging.Logger;

    public class Test extends JavaPlugin {

        @Override
        public void onEnable() {
            FileConfiguration config = getConfig();
            Logger log = getLogger();

            String message = config.getString("message"); //line 13
            log.info(message.replace(config.getString("replace"), config.getString("replace-with"))); //line 14
        }

    }
    Running it this way, given the correct circumstances, will produce the following error:
    Code (Text):
    java.lang.NullPointerException
            at java.lang.String.replace(Unknown Source) ~[?:1.8.0_25]
            at Test.onEnable(Test.java:14) ~[?:?]
            at org.bukkit.plugin.java.JavaPlugin.setEnabled(JavaPlugin.java:316) ~[spigot.jar:git-Spigot-1.7.9-R0.2-207-g03373bb]
            at org.bukkit.plugin.java.JavaPluginLoader.enablePlugin(JavaPluginLoader.java:332) [spigot.jar:git-Spigot-1.7.9-R0.2-207-g03373bb]
    Now, you may think "okay, error occurs on line 14 in Test.java, that's where I have to see if I am dereferencing (calling a method on) a null value". So naturally you go to line 14 in Test.java to see that you are dereferencing the variable log, the variable message, and the variable config. Any of these may be null, right? Right? Wrong!

    Have a look at the stacktrace again. Remember, the first line in the stacktrace points to the place in code where the error first occurs (the last call). Now, what does the stacktrace say? java.lang.String.replace. Okay. Interesting. The error first occurs internally. Something is null internally. This is true. In fact, one of the values we passed to the replace method is null. So either of the two Strings we retrieved from the config may be null. They may not exist in the config. This is now what we are supposed to be looking for.

    How do we know that 'config', 'log' or 'message' weren't null?
    The config variable CANNOT be null in this case. Because if it were, the code would already have errored on line 13.
    Log and message variables are also not null, because, as the stacktrace tells us, the error happens in String.replace.

    Common exceptions, short examples and fixes
    a NullPointerException is thrown whenever an object that is null is being referenced. Lets say we have this code:
    Code (Text):
    //Lets assume we have a predefined Player object named 'p', that's 100% not null.
    Player p = ...;
    String str = config.getString("message");
    p.sendMessage("The length is "+str.length()+".");
    Now, this piece of code may produce an exception. Why? Becasue if the config wont contain the string "message" it will return null, and we cannot invoke methods on a null object, since, well, it's null.
    A basic solution for this case would be simply checking if the config contains 'message', or simply checking if it isn't null:
    Code (Text):

    String str = getConfig().getString("message");
    if(str!=null){
        p.sendMessage("The length is "+str.length()+".");
    }else{
        p.sendMessage("The message is not defined.");
    }

    a NumberFormatException is thrown whenever a string is parsed into a number, any number (float, double, int, etc.) and the string isn't of the correct type. For example, parsing "24" to an integer would be fine since 24 is an integer. It's also a double, so we can parse it into an integer, but parsing "35.4" into an integer would not work since integers are whole numbers. Important: When I say parse, I mean 'Integer.parseInt' method, or 'Double.parseDouble', 'Float.parseFloat', etc. It can be produced in other places:
    Code (Text):
    int i = getConfig().getInt("anumber");
    This may produce a numberformatexception if there's no int under "anumber.
    A basic fix may be surrounding this with try and catch:
    Code (Text):
    int i;
    try{
        i = getConfig().getInt("anumber");
    }catch(NumberFormatException nfe){
        i=0;
    }
    //Or alternatively...
    int i = 0;
    try{
        i = getConfig().getInt("anumber");
    }catch(NumberFormatException ignored){}
     
    an IllegalArgumentException is usually thrown in onCommand() but may occur in more places. This exception is thrown whenever in an argument that doesn't existed is attempted to be accessed. For example, I took the same example from above.
    Code (Text):
    String[] array = {"argument 0", "argument 1", null};
    //This part is okay since there is an array[0] => "argument 0"
    String str = array[0];
    //This part is also okay since there is an argument[2] => null
    String str = array[2];
    //This part will throw an exception since there is no argument[3].
    String st = array[3];
    Lets see when it can be thrown in a bukkit code.
    Code (Text):
    public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args){
        if(commandLabel.equalsIgnoreCase("foo"){
            sender.sendMessage("You entered "+args[0]+"!"); // <==
            return true;
        }
        return false;
    }
    It may be thrown in the line marked with the arrow, since if the sender doesn't specify a first argument, there won't be a first argument and therefore it cannot be accessed.
    A simple solution would be checking if the player did type a first agument:
    Code (Text):
    public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args){
        if(commandLabel.equalsIgnoreCase("foo"){
            if(args.length>=1){
                sender.sendMessage("You entered "+args[0]+"!");
                return true;
            }else{
                sender.sendMessage("You didn't enter a first argument.");
            }
        }
        return false;
    }
  • Loading...
  • Loading...