What's a stacktrace?
![[IMG]](http://proxy.spigotmc.org/f93373f7573fdb8f0129eeebe4f3bc11243da38a?url=http%3A%2F%2Fi39.tinypic.com%2F2nk0013.png)
(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];
//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];
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]](http://proxy.spigotmc.org/7b4528a9f179329659bca12882ec82c081058a03?url=http%3A%2F%2Fi.imgur.com%2FgSLiCne.png)
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
}
}
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
}
}
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)
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)
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
}
}
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
}
}
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]
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]
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:
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:
String str = getConfig().getString("message");
if(str!=null){
p.sendMessage("The length is "+str.length()+".");
}else{
p.sendMessage("The message is not defined.");
}
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()+".");
Player p = ...;
String str = config.getString("message");
p.sendMessage("The length is "+str.length()+".");
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:
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 = getConfig().getInt("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){}
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.
Lets see when it can be thrown in a bukkit code.
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):
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];
//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];
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;
}
if(commandLabel.equalsIgnoreCase("foo"){
sender.sendMessage("You entered "+args[0]+"!"); // <==
return true;
}
return false;
}
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;
}
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;
}