StringBuilderProgramming Practice TutorialsSetsExceptions

Exceptions

If you are like me, you have seen many times how your programs terminate with an error or exception message. Here are some examples of exception messages:

>>> val a = 3
>>> a / 0
java.lang.ArithmeticException: / by zero
>>> val s = "abc"
>>> s.toInt()
java.lang.NumberFormatException: For input string: "abc"
>>> java.io.File("test.txt").forEachLine { println(it) }
java.io.FileNotFoundException: test.txt (No such file or directory)
>>> val s = Array<Int>(100000000) { 0 }
java.lang.OutOfMemoryError: Java heap space

Errors and exceptions

Errors such as OutOfMemoryError indicate a serious failure, where continuing the program makes no sense.

Other exceptions, however, merely indicate an unexpected or abnormal condition in a program. For instance, a mistake in the input data of a program could cause an exception. Such mistakes can be handled: We say that the exception is handled or caught.

For instance, a NumberFormatException might indicate that the user entered an incorrect number, and the correct response would be to print an error message and ask for new input.

A FileNotFoundException means that the file we tried to open does not exist. Depending on the situation, the correct response could be to try a different file name, to ask the user for a different file name, or simply to skip reading the file.

Catching exceptions

The following code asks the user for a number. The function readString returns a string, so we have to convert that to an integer using the toInt() method. If the string is not a number, such as "abc" or "123ab", then the toInt() method throws an exception. We can catch the exception by enclosing the critical part in a try block, and adding a catch block to handle the exceptions we are interested in (catch1.kts):

import org.otfried.cs109.readString

val str = readString("Enter a number> ")

try {
  val x = str.toInt()
  println("You said: $x")
} 
catch (e: NumberFormatException) {
  println("'$str' is not a number")
}

If the try block executes normally, then the catch clauses are skipped. But if somewhere inside the try block (including in any method called, directly or indirectly) an exception is thrown, then execution of the try block stops immediately, and continues in the first case of the catch clause that matches the exception. Here, "matches" means that the exception is the same type as the exception type listed in the case.

The code within a catch case is called an exception handler.

In our example above, if the string str does not represent an integer (for instance, if it is "abc"), then str.toInt throws the exception NumberFormatException. The try block is terminated (and in particular, no value is assigned to x), and execution continues in the catch clause for NumberFormatException. Here are some example runs:

$ kts catch1.kts
Enter a number> 17
You said: 17
$ kts catch1.kts
Enter a number> abc
'abc' is not a number

Exceptions versus error codes

Old programming languages like C do not have exceptions, and so all errors or unusual conditions need to be handled by error codes. In C++, error codes are also still widely used, for instance for compatibility with C.

A simple and elegant method like str.toInt() is impossible without exceptions. We would have to return two results: one Boolean value to indicate whether the conversion was successful, and the Int value itself.

So exceptions allow us to concentrate on the essential meaning of str.toInt(): it takes a string, and returns a number. But the real power of exceptions only appears in the next section…

Exceptions deep deep down

The nice thing about exceptions is that you can also catch exceptions that were thrown inside functions called in the try block.

Going back to our number conversion example, here is version where we convert the string in a separate function (catch2.kts):

fun test(s: String): Int = (s.toDouble() * 100).toInt()

fun show(s: String) {
  try {
    println(test(s))
  }
  catch (e: NumberFormatException) {
    println("Incorrect input")
  }
}

The function test(s) converts the string to a double, but then rounds it off to two decimal places and returns an integer.

When a conversion error occurs, this happens inside test(s), but we can still catch this in the show(s) function:

>>> :load catch2.kts
>>> show("123.456")
12345
>>> show("123a456")
Incorrect input

When an exception occurs (we say that an exception is thrown), then the normal flow of execution is then interrupted, and continues at the nearest (innermost, most recent) catch block where this type of exception is caught (that is, there is an exception handler of the right type).

Let's look at this in more detail and consider the following program (except1.kts):

import org.otfried.cs109.readString

fun f(n: Int) {
  println("Starting f($n) ... ")
  g(n)
  println("Ending f($n) ... ")
}

fun g(n: Int) {
  println("Starting g($n) ... ")
  val m = 100 / n
  println("The result is $m")
  println("Ending g($n) ... ")
}

fun main() {
  while (true) {
    val s = readString("Enter a number> ")
    if (s == "")
      return
    try {
      println("Beginning of try block")
      val n = s.toInt()
      f(n)
      println("End of try block")
    }
    catch (e: NumberFormatException) {
      println("Please enter a number!")
    }
    catch (e: ArithmeticException) {
      println("I can't handle this value!")
    }
  }
}

main()

Here is a run:

$ kts except1.kts 
Enter a number> 25
Beginning of try block
Starting f(25) ... 
Starting g(25) ... 
The result is 4
Ending g(25) ... 
Ending f(25) ... 
End of try block
Enter a number> 0
Beginning of try block
Starting f(0) ... 
Starting g(0) ... 
I can't handle this value!
Enter a number> abc
Beginning of try block
Please enter a number!
Enter a number>
For input value "25", we see beginning and end of try block and the functions f and g. For input value "abc", the toInt method throws an exception, so f is not called. For input value "0", the division inside function g throws an ArithmeticError. As you see, execution continues immediately in the exception handler, without finishing functions g, f, or the try-block.

Throwing exceptions

So far we have only caught exceptions thrown inside some library function. But you can just as well throw exceptions yourself. For instance, let's assume that our function g(n) above should only handle non-negative numbers. We can ensure this by throwing an IllegalArgumentException if the argument is negative. The whole script now looks like this (except2.kts):

import org.otfried.cs109.readString

fun f(n: Int) {
  println("Starting f($n) ... ")
  g(n)
  println("Ending f($n) ... ")
}

fun g(n: Int) {
  println("Starting g($n) ... ")
  if (n < 0)
    throw IllegalArgumentException()
  println("The value is $n")
  println("Ending g($n) ... ")
}

fun main() {
  while (true) {
    val s = readString("Enter a number> ")
    if (s == "")
      return
    try {
      println("Beginning of try block")
      val n = s.toInt()
      f(n)
      println("End of try block")
    }
    catch (e: NumberFormatException) {
      println("Please enter a number!")
    }
    catch (e: IllegalArgumentException) {
      println("I can't handle this value!")
    }
  }
}

main()

Note that exceptions are objects, and are created like any other object, by calling their constructor.

Again, we run it with different inputs:

$ kts except2.kts 
Enter a number> 25
Beginning of try block
Starting f(25) ... 
Starting g(25) ... 
The value is 25
Ending g(25) ... 
Ending f(25) ... 
End of try block
Enter a number> abc
Beginning of try block
Please enter a number!
Enter a number> -17
Beginning of try block
Starting f(-17) ... 
Starting g(-17) ... 
I can't handle this value!

Exceptions are often used to detect errors in the input data.

We can catch the exception at a suitable place in the program and print an error message, or handle the problem in some other way.

When you are debugging a program, you may be confused where certain exceptions come from. In such a case it can be useful to use the method printStackTrace() of the exception object. It prints out the chain of methods that lead to the exception being thrown.

If we change our main function as follows (except3.kts):

fun main() {
  while (true) {
    val s = readString("Enter a number> ")
    if (s == "")
      return
    try {
      println("Beginning of try block")
      val n = s.toInt()
      f(n)
      println("End of try block")
    }
    catch (e: NumberFormatException) {
      println("Please enter a number!")
    }
    catch (e: IllegalArgumentException) {
      e.printStackTrace()
    }
  }
}
then we can see this in action:
$ kts except3.kts 
Enter a number> 35
Beginning of try block
Starting f(35) ... 
Starting g(35) ... 
The value is 35
Ending g(35) ... 
Ending f(35) ... 
End of try block
Enter a number> -17
Beginning of try block
Starting f(-17) ... 
Starting g(-17) ... 
java.lang.IllegalArgumentException
	at Except3.g(except3.kts:16)
	at Except3.f(except3.kts:9)
	at Except3.main(except3.kts:29)
	at Except3.<init>(except3.kts:41)
        ... many omitted lines ...
Enter a number> abc
Beginning of try block
Please enter a number!
Enter a number> 
We can see here that the IllegalArgumentException was thrown in function g (in line 16 of the script), which was called by function f, which was called from the main function.

Assertions

Assertions are conditions in the program that are tested during the execution. If the condition is true, nothing particular happens. If the condition is false, however, an AssertionError exception is thrown. The statement:

assert(condition)
throws an AssertionError if condition is false.

You can also include a message with the assertion.

The purpose of an assertion is to detect errors in your program. (Compare this with the exceptions use above, whose purpose is to detect errors in the input data.)

Consider the following code:

  ... code A computing string s ...
  assert(s.nonEmpty(), "s is empty!")
  ... code B using string s ...
If the code A is correct, then the string s computed by A cannot be empty. We verify that this is indeed true, that is that s is not empty, using an assertion.

The purpose of this assertion is to protect code B. Without the assertion, the following could happen: There is a mistake in code A, and therefore s is empty. This causes some strange crash in code B, and so we start debugging code B. With the assertion, it is immediately clear that the problem is in code A.

So the purpose of assertions is to protect pieces of code from each other, and to isolate problems.

Require

The require(condition) statement is a special form of assertion. It works exactly like assert, but throws an IllegalArgumentException if the condition is false.

This is useful when you need an assertion to test if the arguments of a method are correct: in this situtation you should use require(condition) instead of assert(condition).

The purpose of require is to protect your function from being called with illegal arguments. Without it, you might spend a long time trying to debug your function, when in reality a problem is caused by incorrect argument values given to the function.

Reading and writing files

When reading or writing files, many things can go wrong. The file might not exist, we might not be allowed to write to it, the hard disk may be full, or someone might eject the CD-ROM we are reading from. This means that any serious code the does file input/output needs to worry about catching exceptions.

The following simple script prints all lines in a text file together with the length of each line. If the file doesn't exist or if you are not allowed to read the file, an exception will be thrown. You can catch the exception, print an error message, and continue, instead of letting the program crash (read1.kts):

val fd = java.io.File("project.txt")

try {
  fd.forEachLine {
    println("${it.length} $it")
  }
} 
catch (e: java.io.FileNotFoundException) {
  println("Project file does not exist!")
}
catch (e: java.io.IOException) {
  println("Error reading project file!")
}
If the forEachLine method fails to open the file, it will throw a FileNotFoundException. The loop is not executed; execution jumps directly to the exception handler that prints a message to the user.

FileNotFoundException is a special case of IOException, so the exception matches both catch clauses. However, only one catch clause is executed–the first one that matches. The second catch clause would execute if the first were not present.

It could happen that we can open the file, but the forAllLines method still throws an exception (for instance, because the disk is faulty). This typically generates some sort of IOException. This causes the second catch clause to execute. Exception handlers are often used to recover from errors and clean up loose ends like open files.

Note that you don't need a catch clause for every exception that can occur. You can catch some exceptions and let others propagate.

StringBuilderProgramming Practice TutorialsSetsExceptions