2
$\begingroup$

TL;DR How important is the try ... catch syntax when starting out with learning Java?

Background: I'm teaching A level CS in the UK (age 16-18) and we're using Java as our primary language. The students are already familiar with python.

To teach common data types (such as queues and stacks), we code them. Due to specification considerations, the underlying structure is an array (fixed length, single type). There are a couple of places where we need to code a message that might not always work, such as removing an element from an empty queue or putting an element on a full stack. In python, I'd just return False from the method - it's not perfect but pedagogically it allows me to focus on other things. In Java, I have to return something of the right signature, so if something goes wrong then I need to throw an exception. But then in the main program I need to have a try ... catch structure.

The goal is to use the program to understand the data type, while getting a bit more experience with Java. So I don't want to throw too much at the students all in one go, and try ... catch is not something they are used to. I understand its importance as a programming paradigm but am just trying to sequence the introduction of concepts and figure out where this one should be introduced.

$\endgroup$
3
  • $\begingroup$ Wasn't sure on the tags, please edit to add appropriate ones $\endgroup$ Nov 19, 2023 at 17:27
  • 1
    $\begingroup$ In Java you can return null in place of any object. But you still have to test for it. lists of ints are harder unless a non-negative is expected. $\endgroup$
    – Buffy
    Nov 19, 2023 at 20:09
  • 1
    $\begingroup$ What happens if you use throw without try? [I prefer the Python word raise -- less imperative] Sure you can't error recover. But then the idea of errors should be more fundamental than handling no? $\endgroup$
    – Rushi
    Nov 21, 2023 at 5:59

5 Answers 5

2
$\begingroup$

I'm not sure what the progression is in your education system, but the one I went through to learn programming as a complete novice in Java (~2015, in the USA, at a community college) went roughly like this:

  • Semester 1: hello world, variables, primitive types, operations, conditions, loops, arrays, lists, simple methods like a temperature converter, basic language features like rounding numbers and algorithms like searching for an item in a list and bubble sort.
  • Semester 2: classes, multi-file programs, simple GUI programs (Java swing), basic recursion with integers or arrays, error handling, 2d arrays and basic Java collections.
  • Semester 3: data structures and algorithms, including time complexity, bags, linked lists, stacks, queues, trees, graphs and operations on these data structures like sorting, searching, adding, removing, inserting and internal operations necessary to maintain the integrity of the data structure.

High school students I work with nowadays tend to go through something similar, except they start with Python in semester 1, or maybe with Scratch if they're younger, (not counting the countless possible detours into robotics, Lego Mindstorms, Minecraft modding, etc).

In any case, by the time students learn stacks and queues, it's assumed that the basic language features like error handling have been covered to the extent that students have at least a fundamental level of proficiency/familiarity with them. If this is not the case, it shouldn't be too much of a stretch to cover a topic like error handling, at least in a way that makes it minimally usable within the context of your class.

On a deeper level, if it does feel like a stretch to "ramp up" students on error handling, and they aren't arriving with any experience in it, maybe your curriculum is teaching data structures too early, without adequate fundamentals in place. A data structure course will be painful for any students that aren't comfortable with classes or collections, for example. Rudimentary error handling seems to fit into this prerequisite category.

Method contracts are very important for data structures and algorithms, as this answer points out. Thinking about how a method should behave in exceptional cases is important. Promoting language conventions is important. The habits and skills students develop in reimplementing classic data structures teach key software engineering principles which students will likely use in their career more than the algorithm logic.

Most textbooks for third-semester data structures and algorithms use errors. Returning false to indicate failure won't help you searching for an element in a Boolean collection. Returning null can occasionally be acceptable, but means that data structures can't store null values, and calling code needs to assert or handle the possibility of a failure, effectively approximating try/catch but without the explicitness and safety net. Returning -1 is standard for "index of" searches, because it's not a possible return value.

Sure, some language like JavaScript tend to avoid throwing errors (popping an empty array returns undefined, for example: [].pop()), but I don't think this is necessarily a good way to teach data structures and algorithms in Java. The contracts are less clear and subtle bugs can easily occur. In a course that does use JS or Python to teach data structures, I'd be more inclined to follow the conventions of the language. Along that line of thought, if in doubt when teaching a Java data structures course, it seems reasonable to follow the contracts given by java.util.Collections. The merit of using Java for teaching is the explicitness of the language, types, generics, clear method contracts, named exceptions, class organization, and so forth. Taking pains to avoid any of these concepts feels like a missed opportunity to use the language as it was designed.

Given your circumstances, if you feel try/catch is too much for your students to handle, you can still teach students about errors in general and let them throw uncaught errors. You can provide test harnesses or boilerplate that uses try/catch or JUnit assertions as examples. In some ways, throwing and letting the program fail is less work than handling exceptional cases at every branch after every method call that may fail.

You can simplify exceptions in the following ways, summarizing and extending ideas from above:

  • In general, permit programs to crash, preferably with a clear message. This is the lesser evil than continuing on with unhandled illegal state. If there's a contract violation, students should report it as soon as possible rather than allow the program to potentially silently mess up critical data or crash in a spot far removed from the violation. catch is a necessary but dangerous tool that can be misused to suppress problems.
  • Use only throw and throws and de-emphasize or skip try/catch.
  • Offer test harnesses and boilerplate that handles errors with try/catch or JUnit assertions. From the start, novice Java students won't understand what public static void main(String[] args) means, so they're used to a certain amount of accepting things on faith temporarily.
  • Don't worry too much about error classes and inheritance. Explicit consideration for the exceptional state is the main part, so just using IllegalStateException or even Exception can be permissible for the purposes of simplicity.
  • If you do catch errors, maybe allow Pokemon exception handling for starters, with a cautionary note.
  • If throw is too much, stick to assert. This ensures the students are thinking carefully about pre/post conditions and contracts, without any syntactical complexity or work to be done in the caller. The gotchas here, though, are that assertions are disabled by default, and that assert statements should be side-effect free.

Here are some code snippets to help make the above suggestions more concrete.

Stage 1: just add assert (and compile with -ea):

import java.util.ArrayList;

class Stack<T> {
    private ArrayList<T> stack;
    
    public Stack() {
        stack = new ArrayList<>();
    }

    public void push(T element) {
        stack.add(element);
    }

    public T pop() {
        assert !stack.isEmpty() : "Tried to pop an empty stack";
        return stack.remove(stack.size() - 1);
    }
}

public class Main {
    public static void main(String[] args) {
        var stack = new Stack<Integer>();
        stack.push(1);
        stack.push(2);
        System.out.println(stack.pop());
        System.out.println(stack.pop());
        System.out.println(stack.pop()); // crashes
    }
}

You could go a step further and add pre/post condition assertions to help solidify the contract.

If the asserts stopping the test harness is a problem, you can have students comment out and label these lines by the error they trigger, disable the -ea flag or use separate harness programs for each assertion failure "test".

Stage 2: add throw:

import java.lang.IllegalStateException;
import java.util.ArrayList;

class Stack<T> {
    private ArrayList<T> stack;
    
    public Stack() {
        stack = new ArrayList<>();
    }

    public void push(T element) {
        stack.add(element);
    }

    public T pop() {
        if (stack.isEmpty()) {
            throw new IllegalStateException("Tried to pop an empty stack");
        }

        return stack.remove(stack.size() - 1);
    }
}

Optionally, provide a simple test harness for the student with try/catch:

public class Main {
    public static void main(String[] args) {
        var stack = new Stack<Integer>();
        stack.push(1);
        stack.push(2);
        System.out.println(stack.pop());
        System.out.println(stack.pop());

        try {
            System.out.println(stack.pop());
        }
        catch (IllegalStateException ex) {
            System.err.println(ex); 
        }
    }
}

Hopefully one of these seems doable.

$\endgroup$
2
  • 2
    $\begingroup$ Wow! That's a really detailed answer and thank you especially for providing the code snippets. I'm keen that my students don't learn anything wrong (or bad habits) while wanting not to overload them. I think they can cope with error handling -- certainly as a concept -- it's just me wanting to space out the concepts a little to allow them to get used to the syntax. So in a few weeks I'd hope they'll be try/catching with the best of them, but for the moment I'm trying to chart the smoothest path to that point. Again, many thanks for the detailed answer! $\endgroup$ Nov 21, 2023 at 22:00
  • 1
    $\begingroup$ There's a lot here that I'll keep coming back to as I learn more myself. A crucial point for me (this time) was to de-emphasise the try/catch part of exception handling (your second bullet point). $\endgroup$ Nov 29, 2023 at 21:01
2
$\begingroup$

You may need to teach this concept, but I'd do it at the point it becomes be best option. If things can fail, some sort of test or similar action is needed.

For a list of objects, one can return null in many cases (unless null is a valid value). If a list contains non-negatives you can return -1, say, and test for it. This implies you have already taught other structured statements, such as if-else, etc. So, such statements are no longer a mystery to them.

After you have done this a few times you can discuss the more subtle cases.

First, show them a program that fails (crashes) due to this sort of situation. Then show how a program can recover from such a situation with a try-catch. It shouldn't need to be a big step.

First examples might do no more than give better error messages before halting. Once the structure and execution are clear you can do more with actual recovery.

And, Python can raise and catch exceptions also, in which case you might draw on their knowledge of that language.

$\endgroup$
2
  • 1
    $\begingroup$ Thank you. Returning null sounds like a reasonable placeholder (which is what I'm after). You're right to assume that they know about the various structured statements, and while some might have met python's exception handling then I don't want to assume it and also don't want to introduce too many things in one go. At this stage, we're not writing complicated programs, so just testing for null is fine. $\endgroup$ Nov 19, 2023 at 22:58
  • $\begingroup$ null and un-exceptional exceptions, are two of the biggest mistakes in programming. Does Java have types that can be value or error (but nothing else)? I think it may be possible to make a type with class. $\endgroup$ Nov 26, 2023 at 20:52
2
$\begingroup$

This answer is based on a software engineering point of view, in not teaching things that contradict best practices. (And there are lots of misconceptions and bad practices around exception handling, both in "professional" code as well as in lots of web resources, showing that many developers were trained the wrong way.)

To me the most important concept is that a method has a contract, something it is supposed to do (documented in a Javadoc comment), and that sometimes it cannot fulfill that contract, which it MUST communicate to its caller, and (by modern best practice) SHOULD do by throwing an exception.

As soon as methods arise that can really fail (and that's how I understand your question), please throw exceptions. Special return values like -1 or null have been the standard in the 1970s, and there is good reason why nowadays they have been replaced by exceptions.

Of course, going into details of exception handling will take quite some time in your course. But you can use a shortcut with something like:

Here's a method that can fail. When we detect the failure situation, we throw new Exception("description text");. That's a special way of exitting, telling the caller that we failed. It has to be declared in the method header with a throws Exception clause. Further on, we'll learn that there are lots of different exception types, and various ways what to do in case of an exception, instead of aborting the whole program. But for the moment, that's enough for us to know.

Don't catch exceptions. Catching an exception means that a caller can fulfill its job even though the callee failed, which is rarely true. So this should only be covered under a "Recovering from problems" topic.

If you don't want to follow this path, and instead want to return a special value, make sure that every caller checks whether it got the special return value, and then returns with its special return value and so on. Experience told us that this was very often forgotten, making the following steps produce nonsense, and leading to bugs that were hard to analyze and fix. This finally led to the wide-spread adoption of the "exceptions" pattern.

$\endgroup$
4
  • $\begingroup$ Your opening sentence is something I'm keen to adhere to - while it would be horrendous to teach everything at once, I don't want to simplify to the extent that what I'm saying is wrong or bad practice. But I have a practical problem with your advice to not catch exceptions in that the IDE (IntelliJ) doesn't like that. I shall need to experiment a bit more to see how strongly it objects, but in the meantime is there an easy way to say to Java "Just fail on all exceptions"? $\endgroup$ Nov 20, 2023 at 21:49
  • $\begingroup$ I agree: you don't necessarily need to use try/catch in the caller. This makes it both good and fairly easy, if you type methods throws Exception and teach students to throw Exception("description of contract violation"). The other way to simplify is to not worry too much about the type of exceptions. Your testing harness boilerplate can use try/catch or proper JUnit assertions, as applicable (students need not understand it fully right off the bat). That said, for many data structures it's acceptable to return null when lookup operations fail as many Java util collections do. $\endgroup$
    – ggorlen
    Nov 21, 2023 at 4:44
  • $\begingroup$ @ggorlen Yes, and IMHO it all depends on the contract that you have in mind, whether you expect some element not to exist, and include that in the contract. Or you insist that an object is found, and then not finding one is a failure. It's like the distinction between the words "search" and "find". $\endgroup$ Nov 23, 2023 at 15:44
  • $\begingroup$ I almost accepted this one because your phrase "Don't catch exceptions" prompted an important shift in my thinking, and today my students showed how to get our IDE to stop complaining that I wasn't try/catching the exceptions (by adding throws Exception to the main declaration). So thank you for that phrase! $\endgroup$ Nov 29, 2023 at 21:00
1
$\begingroup$

In Java and many other languages that followed it, Exceptions are un-exceptional: e.g. it is not exceptional to get to the end of a file.

Your example of removing an item from an empty list, is a contract pre-condition violation. Add a method bool list.is_empty(). Then a pre-condition !is_empty(). If the precondition is not met, then throw an error. Don't catch it, as it indicates a programming error: the program should have checked the pre-condition.

More importantly, are exceptions in the syllabus?

$\endgroup$
1
$\begingroup$

Create a type value or error (but nothing else: so not object)

e.g.

class value_or_error {
    private int _value;
    private bool _is_error;

    public bool is_error(){
       return _is_error();
    }
    public int value() {
       if _is_error then throw;
       return _value;
    }
}

Sorry if it does not compile (no javac installed)

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.