Understanding Exceptional Cases and Exception Handling and If Statements

This isn’t a Julia related program, but rather a programming problem in general.

Suppose you’re designing the validation system for a school that issues ID cards to it’s students. One rule is that you don’t want students who have graduated, but still carry a ID card, to enter the school and start using rooms and facilities that are meant for currently enrolled students (for example, you may need a valid ID to login to a computer).

You might write your function like this:

struct Student
    ID::String
    name::String
end 

function validate_students(student_db::Vector{Student},student::Student)
    if !(student in student_db)
       throw(DomainError(student.ID,"Student ID is invalid."))
    end 
    # additional validation
end 

Keep in mind this is a very simplified example to illustrate my problem. I’m fully aware that a database of students won’t be a Vector{Student} and Student would obviously have an inner constructor and validation.

I’ve been reading Exception Handling:When and Why and I’ve been told that Exceptions should only be used in exceptional cases. How do I know if my case is exceptional?.

Is my case an exceptional one? Before using the rooms and facilities we expect the student to be currently enrolled.

The use of if statements presents a problem, it doesn’t stop the workflow of the function.

function validate_students(student_db::Vector{Student},student::Student)
    if !(student in student_db)
       println(student.ID,"Student ID is invalid.")
    end 
    # additional validation -- but we shouldn't be here if the validation above 
                               returns false.
end 

Consider if additional validation has to be done, if we used just if/else statements, we continue on, even after it’s been determined that student should be considered invalid after the first check.

Isn’t return false what you’re looking for?

3 Likes

Sure, that’s one possible solution, but my overall problem is, are exceptions valid? Is my problem an exceptional case?

Consider that in validate_student, if the first if statement returns false (the student is not a valid member), why should I continue checking if a student is valid? It’s already been determined by the first if statement, that it isn’t.

To me exceptions in this case are valid, but I’m still learning, so what I determine as acceptable, may not be true.

You can use return false to exit the function.

function check_if_valid(args...)
   # check 1
   check_1_passed || return false
   # check 2
   check_2_passed || return false
   # check 3
   check_3_passed || return false
   true
end

You could also use “return codes”, so that the caller receives information they can process (e.g., which check failed and why).

Whether the case is exceptional is something you’d have to decide, writing the code.
Consider the alternatives, and which way makes the most sense.

But I probably wouldn’t issue an exception.
This sounds like something that can happen in normal execution of the code, and you want the code to keep running afterwards.
That is, a student enters invalid credentials, you check that they’re valid. If they are, they can book a room or w/e, if not, print an error message and don’t let them do anything they can’t.
But your service should continue running, so that when someone else enters valid credentials, the service can work correctly without needing any restarting.

You could have some try/catch in a loop to catch the invalid errors, but at that point using exceptions aren’t really doing anything for you.

Basically, if I want the program to terminate, I have it throw an error. If it’s something I want to be able to handle (including early stopping), I don’t and use early returns / whatever the situation calls for.

2 Likes

The way I’ve come to think about it, there are two cases:

  • You want to communicate some valid answer to your caller
  • You want to notify your caller that whatever they’ve given you resulted in a state such that you’re unable to give them an answer at all.

In this case, assuming the Student instance is behaving correctly and all assumed invariants hold, communicating the result with true/false gives a perfectly valid and correct answer, so why not do it?

As an example of when I’d consider it ok to throw an exception is e.g. an assumed invariant that Student has a valid field enrollmentDate set, but the instance that’s been given has undef set as a value instead. An invariant has been broken, so it’s ok to throw (but depending on how you want to handle it, returning false is also ok as long as you define and document that this will happen for invalid data). In either case, it’s good practice to at least take note of the fact that an invariant has been broken unexpectedly. It’ll help tremendously with debugging in the future.

1 Like

I am just saying out of my head, so it may be wrong. But you can think about application as a structure that consists of layers and these layers in turn consists of entities that interact with each other. So, this structure has horizontal (inside a layer) and vertical structure (between layers). Now, when you have horizontal interactions, it’s better to use status codes, flags etc, because all entities live in the same context and rather tightly coupled. But for vertical interaction you prefer to throw exceptions, because that just means, that you do not know how to resolve problematic situation inside this layer.

Of course its up to you, to decide, what lays in the same layer and what not. It is context and task dependent and there is no single solution for all situations. In your example, if you consider Student struct and validation to be the part of the same layer, then it’s totally fine to return flags or status codes, because you will process them in the next function which calls validate_students.

On the other hand, if you consider validation utils as a middle layer, which should abstract away database interaction, then it is valid to throw error. Think about it this way: this function can error anyway, for example due to the failure of connecting to the database. So, you inevitably have to wrap this validation in a try/catch block, and since this is the case, it is reasonable to add one more possible exception to process.

Exceptions simplify the final program, but they are very bad for performance. Since julians are obsessed with speed, it is reasonable that exception throwing is not very popular in julian programs. But it is still very useful, when you do not know when exception should be resolved or do not want to propagate it manually. I mean, if you have something like

function validate_students(...)
# ....
  return false
end

function log_students_entry(...)
  status = validate_students(student)
  status || return false
  # ...
end

function interact_with_student(student)
  status = log_students_entry(student)
  if status 
    println("Welcome")
  else
   println("You are not alowed to enter")
 end
end

then it looks like a code smell, since you 1) manually propagate error, 2) tightly coupled together many functions (it would be harder to refactor it, if for example you switch from true/false to 0/1).

1 Like

Another aspect I forgot to mention is that throwing & catching an exception is usually expensive, because that generally means the stack has to be unrolled. This usually takes much more time than returning a value and checking it in the caller.

This is not to say that you should always return a flag or indicator - far from it! Go, for example, usually sets an error flag which you have to check after basically every call that could return an error code, which quickly turns into a bothersome and tedious exercise.

So there’s a tradeoff to be made here, often times between convenience and performance (in those cases where you may want to differentiate multiple kinds of return values, though I’d argue that this is bad API design). In performance sensitive code, avoiding to throw exceptions in a hot loop can be a big deal, since a potentially thrown exception may prevent certain optimizations from happening.

4 Likes