قراءة تتبّعات المكدس (stack traces)
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
Sometimes a program does not give a wrong answer. It stops, and the console fills with red text. Beginners often see a wall of noise and panic. In fact that text is a detective's best gift: it tells you what went wrong, and gives you the exact line and the chain of calls that led to it.
That text is a stack trace. In BEG-014-L05 you met the call stack as a pile of plates. A stack trace is a printed snapshot of that pile at the moment something failed. This lesson teaches you to read it in three moves: type and message, first line of your own code, and the chain that led there.
Why this matters: in a backend job, the first thing you will often receive is a stack trace pasted into a ticket or found in a log. The developers who fix bugs quickly are the ones who read it calmly and open the right line within seconds.
This lesson builds on the call stack (BEG-014-L05), null (BEG-009-L03), and arrays (BEG-015). Exceptions are not formally taught until later; here we only learn to read what a crash prints.
Learning Objectives
By the end of this lesson you will be able to:
- Name the three parts of a stack trace: exception type, message, and call lines.
- Find the first line that points to your own code and open it.
- Say which method called the failing one, and what happened above it.
- Read common messages literally, such as
Index 5 out of bounds for length 5. - Recognize traces that include library frames, and a repeating trace.
- Explain what a
Caused by:section is for.
Written Explanation
Anatomy of a trace
Here is the output of a real crash (Example 1 below):
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
at Report.total(Report.java:5)
at Main.main(Main.java:5)Read it in layers.
- The first line is the headline.
Exception in thread "main"says the problem happened in the main thread (your programs so far have one). Then comes the exception type,java.lang.ArrayIndexOutOfBoundsException, and after the colon the message. The Java documentation says the first line of a printed trace is the throwable's name, a colon and space, and its message (or the name alone if there is no message). - The
atlines are stack frames, one per method call that was active, each with the format Class.method(File.java:line). The Java documentation says the top of this list is the most recent call, which is typically where the exception was created and thrown, and the last line is the bottom of the stack, the start of the sequence of calls (such asmain).
Compare with the call-stack picture from BEG-014-L05: that drawing had the newest frame at the top, and the trace prints in the same order.
The three-move reading
- Read the first line, slowly. What kind of problem (the type) and what exactly (the message)?
- Find the first
atline that is in your code. It is the place in your program closest to the failure. Click it: IntelliJ typically shows theFile.java:linepart as a link that opens that line (check your version). - Read downward to see who called you and with what. This tells you how execution arrived.
In the example, move 1 says "an array index of 5 was used on an array of length 5". Move 2 lands on Report.java:5. Move 3 shows that Main.main called it at Main.java:5.
Read the message literally
Messages are usually precise. Index 5 out of bounds for length 5 means: the program asked for element 5, but an array of length 5 has valid indexes 0 to 4 (as in BEG-015). That is nearly always an off-by-one, or a loop that goes one step too far. You do not need to guess: the message contains the failing index and the length.
A NullPointerException in current JDKs usually says what was null. The Java documentation describes that the message gives a description of the null reference when the information is available. In our run it read Cannot invoke "String.toLowerCase()" because "email" is null. That points at the variable. Note that this detail shows real names only if the code was compiled with debugging information (IntelliJ includes it by default, per its documentation). When it was not, the same message showed "<parameter1>" in our test, and you count parameters instead.
Frames from libraries
Sometimes the top lines are not yours:
Exception in thread "main" java.lang.NumberFormatException: For input string: "1O"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:662)
at java.base/java.lang.Integer.parseInt(Integer.java:778)
at Parse.quantity(Parse.java:3)
at Parse.main(Parse.java:8)The first three frames are Java's own code (java.base is the module that holds it). They tell you where the exception was created, but not where your mistake is. The rule is to skip down to the first line with your class, here Parse.quantity(Parse.java:3): your call to Integer.parseInt received a bad string. The exact library line numbers depend on the JDK version, so do not rely on them.
Long, repeating traces
A trace with the same line repeated hundreds of times is the signature of a runaway recursion (BEG-014-L06):
Exception in thread "main" java.lang.StackOverflowError
at Deep.count(Deep.java:3)
at Deep.count(Deep.java:3)
at Deep.count(Deep.java:3)Our run printed about a thousand such lines. You do not read them all. Look at the first few and the last one, and ask why the method keeps calling itself without stopping.
Caused by
Sometimes one failure is wrapped inside another, and the printout has sections beginning Caused by:. The Java documentation says each cause is printed as Caused by: followed by its own frames, and that ... n more means the remaining frames are identical to the enclosing trace's last frames and not repeated. Read the last Caused by: first, since that is the original problem. You will meet wrapped exceptions properly later; for now, recognize the shape.
From the trace to the cause
The line in the trace is where the program failed, which is not always where the bug is. If Customer.emailDomain crashes because email is null, the bug is probably in whoever passed null. The trace gives you the chain to follow: set a breakpoint at the failing line, then use the Frames tab to select each caller and read its variables.
Examples
Example 1: an index one too far (simple)
// Report.java
static int total(int[] amounts) {
int sum = 0;
for (int i = 0; i <= amounts.length; i++) {
sum += amounts[i];
}
return sum;
}// Main.java
int[] amounts = {5, 10, 15, 20, 25};
System.out.println("Start");
int result = Report.total(amounts);Output:
Start
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
at Report.total(Report.java:5)
at Main.main(Main.java:5)- Type and message: the program used index 5 on a length-5 array.
- First line in your code:
Report.java:5, the linesum += amounts[i];. - Caller:
Main.main, line 5. - Cause: the loop condition
i <= amounts.lengthletsireach 5. The valid indexes are 0 to 4, so the condition should bei < amounts.length.
"Start" printed, so the program ran normally until the failure inside total.
Example 2: following a null up the chain (practical)
static String emailDomain(String email) {
String lower = email.toLowerCase();
return lower.substring(lower.indexOf('@') + 1);
}
static String describe(String name, String email) {
return name + " uses " + emailDomain(email);
}
// in main:
System.out.println(describe("Sara", "sara@example.com"));
String missing = null;
System.out.println(describe("Omar", missing));Output:
Sara uses example.com
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toLowerCase()" because "email" is null
at Customer.emailDomain(Customer.java:3)
at Customer.describe(Customer.java:8)
at Customer.main(Customer.java:14)The failing line is Customer.java:3: email.toLowerCase() on a null. The message even names email. But why was it null? Follow the chain: describe passed its parameter on (line 8), and main (line 14) called it with missing, which was set to null one line earlier. The cause is in main, in the data given to describe, and the failure is two frames above. A sensible fix depends on the rules: reject a null email at the start of describe, or ensure callers never pass one. The trace did the most important job: it gave the path.
Example 3: a crash in the middle of library code (realistic)
static int quantity(String text) {
return Integer.parseInt(text.trim());
}
// in main:
System.out.println(quantity(" 12 "));
System.out.println(quantity("1O")); // letter O, not zeroOutput:
12
Exception in thread "main" java.lang.NumberFormatException: For input string: "1O"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:662)
at java.base/java.lang.Integer.parseInt(Integer.java:778)
at Parse.quantity(Parse.java:3)
at Parse.main(Parse.java:8)The first call worked (" 12 " trimmed to 12). The second crashed. The message quotes the exact string, "1O", and a close look shows a letter O. The first line of your own code is Parse.java:3. The Java documentation says NumberFormatException is thrown when a string is converted to a number but does not have the right format. In a real service, this string came from a user or a file. The sensible response is to validate input before parsing, which a later module covers; for now, notice how much the trace told you without a debugger.
Backend Insight
Backend services print stack traces into logs. When a request fails in production, nobody can set a breakpoint, so the trace is often the only evidence. Experienced developers read a log trace the same way as here: type and message, the first line from the company's own package, and the callers. In a larger application, many of the frames belong to frameworks, so the "first line that is yours" rule saves a lot of time. In a log, start from the first trace, since one failure often triggers more.
When you ask for help, paste the whole trace and the input that led to it. With a reproduction (Lesson 1), that is a complete bug report.
Common Mistakes
Mistake 1: Reading only the last line
The last line is usually main, the bottom of the stack. The place to start is the first line of your own code, near the top.
Mistake 2: Ignoring the message
Index 5 out of bounds for length 5 already contains the two numbers you need.
Mistake 3: Fixing the failing line without asking who sent the bad value
A null parameter, a bad string: the line that failed is the victim, not always the culprit. Follow the callers.
Mistake 4: Starting at the top library frame
If the first lines are java.base/..., do not search there. Find your own class name.
Mistake 5: Trusting library line numbers
They change between Java versions. Your own file's line numbers are the ones to act on.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. In the trace below, name the exception type, the message, and the first line of your own code.
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Stats.perItem(Stats.java:3)
at Stats.print(Stats.java:8)
at Stats.main(Stats.java:13)أظهر الحل A1
A1. Type: java.lang.ArithmeticException. Message: / by zero. First line of your own code: Stats.java:3, in perItem.
A2. In the same trace, which method called perItem, and which called print?
أظهر الحل A2
A2. print called perItem (Stats.java:8). main called print (Stats.java:13).
A3. Which end of a trace is the most recent call, the top or the bottom?
أظهر الحل A3
A3. The top. The first at line is the most recent call, and the last is the bottom of the stack (usually main).
Level B: Application
B1. A message says Index 3 out of bounds for length 3. What are the valid indexes, and what loop-condition mistake commonly causes it?
أظهر الحل B1
B1. The array has length 3, so valid indexes are 0, 1, and 2. Index 3 is one past the end. A common cause is a loop condition written <= instead of <.
B2. For the trace in A1 the code is return total / items; in perItem, and print calls perItem(totals[i], counts[i]) in a loop. What does the trace tell you about items? Where would you set a breakpoint to see which loop pass fails, and what condition would you give it?
أظهر الحل B2
B2. items was 0 when perItem ran, since integer division by zero is what throws ArithmeticException. Set a breakpoint at the division line, or at the call in print (line 8), with a condition like counts[i] == 0. The first stop shows which pass i fails; then inspect totals and counts. In the real run, the first pass printed 25 (100 / 4) and the second crashed.
B3. A trace begins with five java.base/... lines, then Orders.parseQuantity(Orders.java:21). Which line do you open, and why?
أظهر الحل B3
B3. Orders.java:21. The java.base lines are Java's own code where the exception was raised; line 21 is your code that called into it, the nearest place you can change.
Level C: Challenge
C1. Write a tiny program with two methods in which NullPointerException is thrown in the second method because main passed null. Run it, copy the trace, and mark the failure line and the line where the bad value originated.
أظهر الحل C1
C1. Sample answer:
public class NullDemo {
static int length(String s) {
return s.length(); // failure line
}
public static void main(String[] args) {
String name = null; // bad value starts here
System.out.println(length(name));
}
}The trace has NullPointerException with at NullDemo.length(...) first, then at NullDemo.main(...). The failure is at s.length(), the origin at String name = null;. (The exact message text depends on the JDK; JDK 21 includes the null variable's name when compiled with debugging information.)
C2. A trace shows StackOverflowError with the same at Deep.count(Deep.java:3) line repeated about a thousand times. List what you would look at and what you would not.
أظهر الحل C2
C2. Look at the top few lines and the exception type: it is a recursion with no end, so look at the method count and its stopping condition (the base case from BEG-014-L06). Do not read all thousand lines: they are the same line. The fix belongs in the method, where a base case is missing or never reached.
Mini Challenge
Make three small crashes on purpose, one per file: an array index one too far, a null String used with .length(), and Integer.parseInt("12a"). For each, run it and copy the trace. Then write three lines under each: the type and message in your own words, the first line of your code, and the fix.
Check your work: you opened the failing line from the trace link in the IDE and not by searching, and for the null case you named where the null originated.
Summary & Next Lesson
Key takeaways
- A stack trace has three parts: type, message, and
atlines (newest call first). - Read the first line, then find the first line of your own code and open it.
- Read the message literally; it often contains the exact index or string.
- The failing line is not always the cause: follow the callers.
- A trace of one line repeated is a runaway recursion; read
Caused by:last to first.
Next lesson
You have now seen a few exception names. The next lesson, "A catalog of common runtime errors", puts the usual suspects in one table with their typical causes and the first thing to check.
Self-check
- Which line of a trace do you open first?
- What does
Index 5 out of bounds for length 5mean? - Which end of the trace is the newest call?
- Why is the failing line not always the bug?
- What does a long repeated trace suggest?
أظهر الإجابات
- The first line that points to your own code.
- Index 5 was used on an array of length 5; the valid indexes are 0 to 4.
- The top. The bottom is usually
main. - A bad value may come from a caller, so the cause can be further down the trace.
- Recursion that does not stop (
StackOverflowError).
References
- Oracle, Throwable (Java SE 21 API). Supports the first-line format of a printed trace, that the top element is the most recent call and the last is the bottom of the stack, and the
Caused by:and... n moreconventions. - Oracle, ArrayIndexOutOfBoundsException. Supports: thrown when the index is negative or not less than the array length.
- Oracle, NullPointerException. Supports the cases in which it is thrown and the description of the null reference in the message when information is available. The
<parameter1>behavior without debugging information comes from our own test run, not from the page. - JetBrains, Debug code. Supports: the "Generate debugging info" compiler option is on by default.
- Oracle, NumberFormatException. Supports: thrown when a string is converted to a numeric type without the right format.
- All trace output in this lesson was produced by compiling and running the code with JDK 21.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.