CS 346 (W23)
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Error Handling

Imagine that we specify a command-line option that is not supported by our program. What would happen?

 $ ./rename.kts truncate:true prefix:a file1
 file1 renamed to afile1

Hang on. truncate:true is not supported by our code (public:/console/rename).

Here’s the code. Why did it work?

if (options.contains("capitalize")) renamed = renamed.toUpperCase()
if (options.contains("prefix")) renamed = options.get("prefix") + renamed 
if (options.contains("suffix")) renamed = renamed + options.get("suffix")

Ok so it didn’t apply truncate to our options, it was just skipped. We don’t check for invalid options in the code!

We should probably be iterating over the options, and processing every argument that is passed in. Expand to include an ”else” case.

for ((key, value) in options) { 
  when (key) { 
    "capitalize" -> renamed = renamed.toUpperCase() 
    "prefix" -> renamed = value + renamed
    "suffix" -> renamed + value
    else -> "Unknown option $key:$value" 
  } 
} 
$ ./rename.kts truncate:true prefix:a file1
Unknown option truncate:true
file1 renamed to afile1

What happens if we specify a filename that doesn’t exist?

$ ./rename.kts prefix:a suffix:b capitalize:true file2
file2 renamed to AFILE2B
$ ls A*
zsh: no matches found: A*

The file didn’t exist, but it also didn’t report errors. What happened?

File(file).renameTo(renamedFile)

The File.renameTo() method returns true or false based on the results of the operation, but but we didn’t check the return value. We could just do a quick if... then... else but let’s consider a more robust way of handling errors.

Exception Handling

Kotlin uses exceptions to indicate that an operation has failed. The mechanism is similar to other languages: if an error is detected, an exception is created and ‘thrown‘, and then ‘caught‘ and consumed by error handling code further up the stack.

Exceptions mechanisms can be either checked or unchecked:

  • Unchecked means that exceptions are not checked at compile-time. If an exception is thrown by some function, it is passed up the call stack and may or may not be handled by a corresponding ‘catch‘ block. e.g. C++
  • Checked exceptions are checked at compile-time. Exceptions are declared with each function in the call-chain and must be handled by a corresponding ‘catch‘ block.

Like C++, Kotlin supports unchecked exceptions.

Sidebar: checked exceptions were one of those features in Java that seemed like a good idea when they sere introduced. However, they were misused in Java, and often used a replacement for return values that could have been easily checked and handled in-place. This means that Java programs are often littered with nonsensical code like this:

try { 
	httpconnect.setRequestMethod("POST") 
} catch (ProtocolException e) {
	throw("This should have been a return value!") 
} 

In any other language we typically use exceptions when we want to propogate the error up the call stack. In Java, they serve double-duty, and end up being used as a generic error handling mechanism. In Kotlin, you can use exceptions when they make sense but they’re not required.

The Exception class is built-in to the Kotlin Std Library: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-exception/

An exception typically contains a message, and the stack trace from the point where it was generated.

The Exception class represents a generic exception, but we also have specialized exception classes derived from it:

  • RuntimeException
  • NoSuchPropertyException
  • IllegalArgumentException

If you’re calling code (e.g. in a library) that could potentially fail, you should use a try... catch block to enclose the code. In the example below, numerator / denominator could result in a divide-by-zero error, which would generate an ArithmeticException exception. If this occurs, the catch block will be called to handle that exception.

fun divideOrZero(num: Int, den: Int): Int { 
  try { 
	  return num / den
  } catch (e: ArithmeticException) { 
  	return 0 
  } 
} 

Without the try...catch any exception that was generated would be passed up the stack to the first function that explicitly handled that exception.

In your own functions, you can throw an exception in response to an error:

throw IllegalArgumentException("Invalid parameter")

The caller of your function would then be responsible for checking for an exception.

Example: rename using exceptions

Recall the ‘rename‘ example, when we specified a non-existent file:

$ ./rename.kts prefix:a suffix:b capitalize:true file2
file2 renamed to AFILE2B

$ ls A*
zsh: no matches found: A*

The File.renameTo() method returns true or false indicating if the rename worked, but we didn’t check it. Let’s change that.

Let’s check the return status and throw an exception if we cannot access the file (i.e. it doesn’t exist).

fun applyOptions(files:List<String>, options:HashMap<String, String>) { 
  for (f in files) { 
    // ... 
    if (!f.renameTo(File(newname))) { 
    	throw(IOException("Cannot rename $f")) 
    } else {
    	println("$f renamed to $newname")
    } 
  } 
} 

Let’s try running it now and see what happened when we try to rename a file that doesn’t exist.

 $ ls file*
 zsh: no matches found: file*

 $ ./rename prefix:a file1
 Exception in thread "main" java.io.IOException:
  "Cannot rename file1"
  at RenameKt.applyOptions(rename.kt:53)
  at RenameKt.main(rename.kt:20)

The exception is bring thrown correctly (and even includes our custom error message!). However, the app is exiting. Let’s catch and deal with the error instead.

The behaviour we would prefer if we catch the error is to report it, and then continue processing without exiting the app prematurely.

fun applyOptions{} {
  for (f in files) { 
    try {
    	renameFile(f, newname) 
    	println("$f renamed to $newname") 
    } catch (e:Exception) { 
    	println("Unable to rename $f") 
    } 
	} 
}

fun renameFile(source:String, dest:String) { 
	if (!File(source).renameTo(File(dest))) { 
		throw(IOException("Cannot rename $source")) 
	} 
}