Week 1 | Lesson 4

I/O, Errors and Exceptions

File I/O, Exception Handling, Error Handling Strategies



Β© 2026 by Monika ProtivovΓ‘

Application Error Handling

Application Errors - General Concepts

Applications can encounter various types of errors during their execution. These errors can be due to user input, system state, or business logic violations.

So far, we have not paid great attention to the errors that can occur in our applications.

Under normal operation, most application calls will not result in an exception. But there are valid reasons why an exception might be thrown:

  • Input validation
    For example, we may want to validate user inputs, such as valid JSON request body, or valid query parameter values. Example: non empty list, string, valid email address, etc.
  • State validation
    For example, we may want to validate the state of the application, such as whether a user is logged in, or whether a resource exists.
  • Business logic validation
    For example, we may want to validate the business logic of the application, such as whether a user has permission to perform an action, or whether a resource is available.

You also need to account for unexpected errors, such as network failures, database errors, or other unforeseen issues, as well as third-party (library, service) failures.

Built-in Functions

Kotlin comes with a set of convenience functions to help us validate inputs and application state and raise errors.

Here are few examples of validation functions:

  • require(conditon) { "Message if condition not met" }
    Throws IllegalArgumentException with provided message if the condition is false.
  • requireNotNull(value) { "Message if value is null" }
    Throws IllegalArgumentException with provided message if the value is null.
  • check(conditon) { "Message if condition not met" }
    Throws IllegalStateException with provided message if the condition is false.
  • checkNotNull(value) { "Message if value is null"
    Throws IllegalStateException with provided message if the value is null.
  • error("Message")
    Throws IllegalStateException with provided message unconditionally.

Exceptions

and error handling

What is an Exception

Exceptions are events that disrupt the normal flow of program execution.
  • They can arise due to various types of errors such as IO errors, arithmetic errors, null pointer access, etc.
  • Exception is just another type of Kotlin object:
    • Exception is an instance of a Exception class or one of its subclasses.
    • There are several subclasses of Exception provided in Kotlin by default, but we can create our own by extending these superclases.
    • There are two types of exceptions: Checked or Unchecked
  • The Exception object usually carries information about the error that occurred.
  • Exception handling allows us to control the program flow and prevent the program from terminating abruptly, which leads to a more robust and fault-tolerant software.

Checked vs Unchecked Exceptions

The are two types of exceptions you can encounter in Java and Kotlin: checked and unchecked exceptions.

Checked Exceptions

These are exceptional conditions that a well-written application should anticipate and recover from.

  • Classes that extend Throwable except RuntimeException and Error
  • Checked at compile-time
  • Compiler forces programmer to handle them
  • Must use try-catch or throws declaration
  • Example: FileNotFoundException

Unchecked Exceptions

These represent defects in the program (bugs), often invalid arguments passed to a non-private method.

  • Classes that extend RuntimeException and Error
  • Checked at runtime, not compile-time
  • Not required to be handled explicitly
  • Can still be caught with try-catch
  • Examples: NullPointerException, ArithmeticException

Common Exceptions

Java and Kotlin provide a rich set of built-in exceptions that can be used to handle common error conditions.
  • NullPointerException
    Thrown when an application attempts to use null in a case where an object is required.
  • IllegalArgumentException
    Thrown to indicate that a method has been passed an illegal or inappropriate argument.
  • IllegalStateException
    Thrown to indicate that program reached an illegal state, such as illegal combination of parameters or illegal sequence of method calls.
  • IndexOutOfBoundsException
    Thrown to indicate that an index of some sort (such as an array or string) is out of range.
  • NumberFormatException
    Thrown when an attempt is made to convert a string to a numeric type, but the string does not have the appropriate format.
  • IOException
    Thrown when an I/O operation fails or is interrupted.

Defining Custom Exceptions

It is often a good idea to define custom exceptions for specific error conditions in your application.

Defining custom exceptions allows you to provide more meaningful error messages and handle specific error conditions in a more granular way. It also allows you to treat same class of errors consistently across your application.

Here are few examples of programmer-defined exceptions:

  • InvalidCredentialsException
    Thrown when a user fails to authenticate.
  • UnauthorizedAccessException
    Thrown when a user is not authorized to perform an action.
  • ResourceNotFoundException
    Thrown when a requested resource is not found.
  • InvalidInputException
    Thrown when the input provided by the user is invalid.
  • ConflictException
    Thrown when the action would result in a conflict (duplicate).

Handling Exceptions

throwing and catching exceptions

Handling exceptions

Kotlin provides a standard mechanisms to handle exceptions using try, catch, finally blocks.
try { // code that might throw an exception } catch (ex: ExceptionType) { // code to handle the exception } finally { // code that will execute irrespective of an exception occurred or not }
  • The try block contains the code that might throw an exception.
  • The catch block contains the code that is executed when an exception of given type occurs in the try block.
  • The finally block contains the code that is always executed, regardless of whether an exception occurs or not.

Throwing exceptions

"Throwing an exception" refers to the process of creating an instance of an Exception (or its subclass) and handing it off to the runtime system to handle.

It's a way of signaling that a method cannot complete its normal computation due to some kind of exceptional condition.

There are two keywords associated with throwing exceptions:

  • The throw keyword is used to "emit" an exception from any block of code. We can throw either checked or unchecked exceptions.
  • If you want to declare that a method may throw an exception, you can use the @Throws annotation.
  • Declaring that a method throws an exception is a way of signaling to the caller that the method may not complete normally, so that the caller can handle it.

Throwing and handling exceptions

// Class that throws exception class Car(private var fuelKm: Int) { @Throws(NoFuelException::class) fun drive(driveKm: Int) { var driveKm = driveKm while (driveKm > 0) { if (fuelKm <= 0) { // exception in thrown on car out of fuel event throw NoFuelException() } else { println("drove 1 km") fuelKm-- driveKm-- } } } } // NoFuelException exception definition class NoFuelException : Throwable("The car is out of fuel!") fun main() { val car = Car(3) try { car.drive(4) } catch (e: NoFuelException) { // compiler will force catch block here println(e.message) // somehow handle car out of fuel situation } }

Throwing and handling exceptions

In this example we try to divide number by 0, which is illegal. The compiler will let us compile this code, because there is no checked exception. When executed, the program will end with:

Exception in thread "main" java.lang.ArithmeticException: / by zero
fun main() { val number = 100 / 0 // will end with "Exception in thread "main" java.lang.ArithmeticException: / by zero" }

However, we can still handle the unchecked exception too, we are just not warned by the compiler.

fun main() { val dividend = 100 val divisor = 0 try { val quotient = dividend / divisor } catch (e: Exception) { println(e.message) } }

Exceptions Exercise

Hands-on Practice

Practice: Error Handling

Practice exception handling and validation in the Learning App.

I/O

Paths and Resources

Paths and resources

Understanding how to access files and resources in Kotlin applications.

To access a file, we need to obtain its path reference as URI (Uniform Resource Identifier), which is a string of characters used to identify a resource. You can also use URL, which is a specific type of URI that identifies a resource on network location.

There are two types of paths:

  • Absolute path - A complete path to a file or directory from the root of the file system.
  • Relative path - A path to a file or directory relative to the current working directory.

Resources are files that are bundled with your application, typically in the src/main/resources directory.

import java.io.File fun main() { val absoluteFile = File("/path/to/file.txt") // Absolute path val relativeFile = File("file.txt") // Relative path // Getting current working directory val currentDir = File(".") println("Current directory: ${currentDir.absolutePath}") // Accessing resources val resourceUrl = object {}.javaClass.getResource("/example.txt") if (resourceUrl != null) { val resourceFile = File(resourceUrl.toURI()) println("Resource file: ${resourceFile.absolutePath}") } }

Working with Files

Working with Files

Kotlin provides a kotlin.io library of functions to work with files which is actually an extension of Java libraries.

This is where it gets a little messy. When we work with files in Kotlin, we need to understand a little bit about Java history.

Java comes with two libraries for working with files and directories:

  • java.io (legacy)
    Provides classes for reading and writing files, and working with directories. This library contains the File class that represents a file or directory path.
  • java.nio.file ("modern")
    Provides a more modern way to work with files and directories and was introduced in Java 7.

Kotlin also provides its own functions for working with files in the kotlin.io package. These functions are implemented as extension functions on top of the Java libraries, specifically the File class.

Reading Files

Some of the kotlin.io functions to read files are ...
import java.io.File fun main() { val file = File("example.txt") // Reading entire file as text try { val content = file.readText() println("File content: $content") } catch (e: Exception) { println("Error reading file: ${e.message}") } // Reading lines try { val lines = file.readLines() lines.forEachIndexed { index, line -> println("Line $index: $line") } } catch (e: Exception) { println("Error reading lines: ${e.message}") } // Processing line by line try { file.forEachLine { line -> println("Processing: $line") } } catch (e: Exception) { println("Error processing lines: ${e.message}") } }
  • readText
    Reads the entire file as a String.
  • readLines
    Reads the file as a list of lines.
  • readBytes
    Reads the entire file as a byte array.
  • forEachLine
    Iterates through each line of the file.

Writing Files

Some of the kotlin.io functions to write files are ...
import java.io.File fun main() { val file = File("output.txt") // Writing text to file try { file.writeText("Hello, World!\nThis is a new file.") println("File written successfully") } catch (e: Exception) { println("Error writing file: ${e.message}") } // Appending text to file try { file.appendText("\nAppended line.") println("Text appended successfully") } catch (e: Exception) { println("Error appending to file: ${e.message}") } // Using PrintWriter try { file.printWriter().use { writer -> writer.println("Line 1") writer.println("Line 2") writer.println("Line 3") } println("File written with PrintWriter") } catch (e: Exception) { println("Error with PrintWriter: ${e.message}") } }
  • writeText
    Writes the specified text to the file.
  • writeBytes
    Writes the specified byte array to the file.
  • appendText
    Appends text to the file.
  • printWriter
    Returns a PrintWriter for writing to the file.

Other File Operations

Other file operations Kotlin makes available for us include ...
import java.io.File import java.util.Date fun main() { val file = File("example.txt") println("File exists: ${file.exists()}") println("Absolute path: ${file.absolutePath}") println("Is file: ${file.isFile}") println("Is directory: ${file.isDirectory}") println("Can read: ${file.canRead()}") println("Can write: ${file.canWrite()}") println("Can execute: ${file.canExecute()}") if (file.exists()) { println("Last modified: ${Date(file.lastModified())}") println("File size: ${file.length()} bytes") } // Creating directories val directory = File("new-directory") if (!directory.exists()) { val created = directory.mkdir() println("Directory created: $created") } // Listing files in directory val currentDir = File(".") currentDir.listFiles()?.forEach { child -> println("${if (child.isDirectory) "[DIR]" else "[FILE]"} ${child.name}") } }
  • exists
    - Checks if the file exists.
  • absolutePath
    - Returns the absolute path of the file.
  • canonicalPath
    - Returns the canonical path of the file.
  • isFile
    - Checks if the file is a regular file.
  • isDirectory
    - Checks if the file is a directory.
  • canRead
    - Checks if has read permissions.
  • canWrite
    - Checks if the file has write permissions.
  • canExecute
    - Checks if the file execute permissions.
  • lastModified
    - Returns the time the file was last modified.

Input and Output Stream

Input and Output Stream

Working with InputStream and OutputStream classes.

When working with programs and libraries, you may come across code that uses InputStream and OutputStream classes.
These classes are used to read and write data from and to a source, such as a file, network connection, or memory.

import java.io.FileInputStream import java.io.FileOutputStream import java.io.IOException fun main() { try { val inputStream = FileInputStream("input.txt") val outputStream = FileOutputStream("output.txt") // Reading from input stream val buffer = ByteArray(1024) var bytesRead = inputStream.read(buffer) while (bytesRead != -1) { // Writing to output stream outputStream.write(buffer, 0, bytesRead) bytesRead = inputStream.read(buffer) } // Always close streams inputStream.close() outputStream.close() println("File copied successfully") } catch (e: IOException) { println("IO Error: ${e.message}") } } // Better approach using 'use' for automatic resource management fun copyFileWithUse() { try { FileInputStream("input.txt").use { input -> FileOutputStream("output.txt").use { output -> input.copyTo(output) } } println("File copied using 'use'") } catch (e: IOException) { println("IO Error: ${e.message}") } }
  • InputStream is an abstract class that represents an input stream of bytes.
  • OutputStream is an abstract class that represents an output stream of bytes.

Practice

Practice: Shape File I/O

Let's use what we have learned.

Load shapes from a shapes.csv file provided in resources and save analysis results.

Exercise: AI Code Generation & Review

Exercise: Build a Log File Analyzer

You will use AI to generate a Kotlin program that analyzes server log files. Your goal is not just to get working code, but to critically evaluate what the AI produces.

Requirements

Create a Kotlin program with a main function that:

  • Reads a log file from a specified path
  • Parses each log line to extract: timestamp, HTTP method, endpoint, status code, response time
  • Calculates and prints statistics:
    • Total requests processed
    • Average response time
    • Count of requests by status code (200, 404, 500, etc.)
    • Top 5 slowest endpoints
    • List of all failed requests (status >= 400)
  • Handles errors gracefully (missing files, malformed lines)

Sample Input Format

Each log line follows this format:

2024-01-15 10:23:45 GET /api/users 200 45ms
2024-01-15 10:23:47 POST /api/orders 201 123ms
2024-01-15 10:23:50 GET /api/products 404 12ms
2024-01-15 10:23:52 GET /api/users/123 500 89ms
2024-01-15 10:23:55 DELETE /api/orders/456 204 67ms

Expected Output Example

=== Log Analysis Results ===
Total Requests: 5
Average Response Time: 67.2ms

Status Code Distribution:
- 200: 1
- 201: 1
- 204: 1
- 404: 1
- 500: 1

Top 5 Slowest Endpoints:
1. POST /api/orders - 123ms
2. GET /api/users/123 - 89ms
...

Failed Requests (4xx/5xx):
- GET /api/products (404) - 12ms
- GET /api/users/123 (500) - 89ms
πŸ’‘ ️ Test your solution with various edge cases: empty files, malformed lines, missing fields, very large files.

How to Complete This Exercise

Follow these steps to generate and critically evaluate AI-generated code.

Step 1: Generate with AI (10 minutes)

  1. Choose your AI tool (ChatGPT, Claude, GitHub Copilot, etc.)
  2. Copy the requirements from the previous slide
  3. Ask the AI to generate a Kotlin solution with a main function
  4. Review the generated code - does it look reasonable?
  5. Create a test log file with sample data
  6. Run the code and see if it works

Step 2: Critical Review (15 minutes)

  1. Go through the review checklist (next slide)
  2. Test edge cases: empty file, malformed lines, missing file
  3. Check for the common pitfalls AI makes
  4. Identify what needs to be fixed or improved
  5. Make necessary corrections yourself
  6. Document what the AI got wrong

Sample Test Data

Create a file called server.log with this content:

2024-01-15 10:23:45 GET /api/users 200 45ms
2024-01-15 10:23:47 POST /api/orders 201 123ms
2024-01-15 10:23:50 GET /api/products 404 12ms
2024-01-15 10:23:51 INVALID LINE
2024-01-15 10:23:52 GET /api/users/123 500 89ms
2024-01-15 10:23:55 DELETE /api/orders/456 204 67ms
2024-01-15 10:23:58 GET /api/products 200 23ms
2024-01-15 10:24:01 POST /api/users 500 156ms
2024-01-15 10:24:03 GET /api/orders 200 34ms
⚠️ ️ Notice the invalid line on line 4. Does the AI-generated code handle this gracefully?
❗ ️Learning Goals: This exercise is about developing critical review skills, not just getting working code. Pay attention to what the AI does well and where it falls short. Professional developers must be able to evaluate and improve AI-generated solutions.

Code Review Checklist

Use this checklist to systematically evaluate the AI-generated code. Don't just check if it runs - evaluate its quality.

Correctness & Logic

  • ☐ Does it parse all log fields correctly?
  • ☐ Are calculations accurate (averages, counts)?
  • ☐ Does it correctly identify failed requests (status >= 400)?
  • ☐ Are the top 5 slowest endpoints sorted correctly?
  • ☐ Does it handle the response time parsing (removing 'ms')?

Error Handling

  • ☐ What happens if the file doesn't exist?
  • ☐ What happens with a malformed line?
  • ☐ What if a field is missing?
  • ☐ What if the file is empty?
  • ☐ Are error messages helpful?

Edge Cases

  • ☐ Works with empty log file?
  • ☐ Handles very large files efficiently?
  • ☐ Works with single log entry?
  • ☐ Handles unusual status codes (e.g., 301, 503)?

Code Quality

  • ☐ Is the code readable and well-organized?
  • ☐ Are variable names meaningful?
  • ☐ Is it properly structured (not one giant function)?
  • ☐ Does it use appropriate data structures?
  • ☐ Are there any code smells or anti-patterns?

Performance

  • ☐ Does it read the file efficiently?
  • ☐ Are there unnecessary iterations?
  • ☐ Does it handle large files without loading everything into memory?
  • ☐ Are sorting operations efficient?

Kotlin Idioms

  • ☐ Uses Kotlin features appropriately (nullable types, collections, etc.)?
  • ☐ Follows Kotlin naming conventions?
  • ☐ Uses extension functions where appropriate?
  • ☐ Leverages standard library functions (map, filter, groupBy, etc.)?
πŸ’‘ ️ Test each item methodically. Create specific test cases for edge cases and error conditions. Don't assume the code works just because it compiles or produces some output.

Common AI Mistakes to Look For

AI tools often make predictable mistakes. Knowing what to look for helps you review code more effectively.

Typical Issues in This Exercise

  • Poor error handling
    AI might not handle missing files or throw generic exceptions that crash the program.
    // Bad: crashes on missing file val lines = File("server.log").readLines() // Better: handles error gracefully val file = File("server.log") if (!file.exists()) { println("Error: Log file not found") return }
  • Ignoring malformed lines
    AI might silently skip invalid lines or crash when parsing fails.
    // Bad: crashes on invalid format val parts = line.split(" ") val statusCode = parts[4].toInt() // Better: validates and handles errors val parts = line.split(" ") if (parts.size < 6) { println("Skipping malformed line: $line") continue }
  • Incorrect parsing
    Might forget to remove 'ms' from response time or parse fields in wrong order.
  • Off-by-one errors
    Top 5 might show 4 or 6 results, or include/exclude the wrong status codes.

General AI Code Problems

  • Inefficient algorithms
    Might iterate multiple times instead of processing in one pass.
  • Overly complex solutions
    AI sometimes produces unnecessarily complicated code when simpler would work.
  • Missing null safety
    Might use !! (force non-null) or not handle nullable types properly.
  • Not using standard library
    Reinvents functionality that exists in Kotlin standard library (groupBy, averageBy, etc.).
  • Poor naming
    Generic names like data, temp, x instead of meaningful names.
  • Incomplete requirements
    Might miss one of the statistics or format output differently than specified.
⚠️ ️ AI-generated code often works for the "happy path" but fails on edge cases. Always test with invalid inputs, empty data, and boundary conditions.

Discussion & Learning Points

Reflect on what you learned about AI capabilities, limitations, and how to work effectively with AI coding assistants.

Questions to Consider

  • What did the AI do well? What aspects of the solution were correct and well-structured?
  • What did the AI get wrong or miss completely?
  • How much effort was required to review and fix the code compared to writing it from scratch?
  • Would you have made the same mistakes as the AI? Different mistakes?
  • What would have happened if you deployed this code without careful review?
  • How can you improve your prompts to get better results from AI?
  • What types of problems is AI particularly good or bad at solving?

Key Takeaways

  • AI is a powerful tool, not a replacement
    It can accelerate development but requires human oversight and critical thinking.
  • Testing is essential
    Never trust AI-generated code without thorough testing, especially edge cases.
  • Code review skills matter more than ever
    Your ability to spot issues and improve code is increasingly valuable.
  • Understanding beats copying
    You must understand what the code does to know if it's correct and maintainable.

Best Practices for AI-Assisted Development

  • Start with clear, detailed requirements
  • Generate code for well-defined, isolated problems
  • Review every line before committing
  • Test with edge cases and error conditions
  • Refactor AI code to match your project's style
  • Use AI to explore approaches, not blindly accept solutions
  • Learn from AI mistakes to improve your own code
❗ ️Your Competitive Advantage: As AI coding tools become ubiquitous, your competitive advantage isn't whether you use AI - it's how effectively you use it. Developers who can rapidly generate, evaluate, test, and improve AI-generated solutions will be far more productive than those who either avoid AI or blindly trust it. Critical thinking, code review skills, and deep understanding of software engineering principles matter more than ever.

Graded Exercises: Error Handling

Complete these exercises in the Grader App for auto-grading and feedback.

Open the Grader App, pick an exercise, write your solution, then Save Draft β†’ Test β†’ Review β†’ Submit.



Assignment: Shape File I/O

Assignment: Shape File I/O

Load shapes from CSV file and save analysis rankings to CSV.

In this assignment, you'll add file I/O capabilities to the Shape Analysis system:

  • Create FileUtils object with generic CSV read/write functions
  • Implement readCsvFile(fileName: String): List<List<String>> - generic CSV reader
  • Implement saveCsvFile(data: List<List<String>>, fileName: String): File - generic CSV writer
  • Create loadShapes(fileName: String): List<Shape> - domain-specific function
  • Create saveRankings(rankings: List<ShapeRanking>, fileName: String): File
  • Load shapes from shapes.csv in src/main/resources/
  • Run analysis and save rankings to rankings.csv

Key Concepts:

  • Resource Loading - Accessing files from classpath
  • CSV Parsing - String manipulation with split() and column indexing
  • File Operations - File.readLines(), File.writeText()
  • Type Conversion - toDouble(), ShapeType.valueOf()