The following are the various topics that we’ll look at in this blog series: –
- Why do we raise exceptions?
- General principles when handling/raising exceptions
- Different types of exceptions – How do we choose?
- Inheritance in exception classes – How do we use this to our advantage?
In the previous blog, we have already looked at why we need to raise exceptions.
In this blog, we will look at the second topic – General principles when handling/raising exceptions.
Think locally, don’t make assumptions about the calling program
While implementing a programming unit, we make assumptions about the caller. Sometimes, these assumptions become invalid during the course of time. It is also possible that we have made the incorrect assumptions about the caller in the first place. We therefore not only need to think about what to do if those assumptions are true but more importantly what to do if the assumptions are broken. This not only helps us make robust programs but also programs that are more re-usable.
This can be illustrated by an example program depicted by the following sequence diagram. Please note that some details have been omitted out for the sake of clarity: –
- In this example, the calling program first creates an instance of the library (lo_library) and the student lo_student1 (Divyaman).
- The calling program then registers the student in the library.
- The calling program then creates the book – lo_book1 (Catcher in the Rye) and adds it to the library stock.
- The calling program then loans-out the book lo_book1 (Catcher in the Rye) to the student lo_student1 (Divyaman).
The execution of the above program is successful because the pre-requisite for lending a book to a student (the student must be registered as a member of the library) has been met. However, the caller, could mistakenly skip registration of the student. What will happen in that case? Let’s take a look at the source code of the ZCL_LIBRARY=>LEND_BOOK method.
The code above contains no check for the registration status of the student trying to loan-out the book from the library. If the student was not registered by the calling program, the library will still end up lending the book to the student. The method was written with the assumption that the student passed to it as a parameter is a registered member of the library.
In order to prevent this, we introduce logic within the ZCL_LIBRARY=>LEND_BOOK method to check whether the student is registered. If not, we raise an exception. Subsequently, in the calling program, we handle this exception and print an appropriate message on the screen in case the exception occurs. This is shown in the source code below: –
Now, when the caller tries to lend books to a student who is not registered as a member, an exception is raised by the ZCL_LIBRARY=>LEND_BOOK method. The caller of this method (ZCL_STUDENT=>LOAN_BOOK) in turn propagates it to the calling program ZEXC_HNDL_EXMPL2 where the exception is handled to print an appropriate message.
Now, when the program is run the following message is printed on the screen: –
Catcher in the Rye could not be loaned.
The program in the previous blog also illustrates this principle.
But this will never happen
This ties in very closely to the principle ‘Think locally, don’t make assumptions about the calling program’. However, here we are talking about assumptions with regards to the state of the program or the underlying data store. While coding a program unit, we might assume that the program will be in a certain valid state or the data in the under-lying data store will be in a certain valid state at the point where the program unit is executed. However, we often fail to describe the behavior of the program unit should those assumptions prove to be untrue. In order to make our programs more robust, this is a factor that ought to be considered.
Let’s consider the following example: –
Some details have been left out of the above diagram for the sake of brevity.
- Here, we create a library instance – lo_library.
- We created three student instances – lo_student1 (Divyaman), lo_student2 (Anubhav) and lo_student3 (Jack). The three students are registered with the library.
- Three books are created – lo_book1 (Catcher in the Rye), lo_book2 (The Kite Runner) and lo_book3 (A Thousand Splendid Suns). These are then added to the library stock.
- The books are then loaned as follows: –
Catcher in the Rye -> Divyaman
The Kite Runner -> Anubhav
A Thousand Splendid Suns -> Jack
- The student and the book loan information is then written into a text file in the following format: –
SN-Student 1’s Name
SN-Student 2’s Name
SN-Student N’s Name
BL-<Book 1’s Title>-<Book 1’s Author>-<Student Member Name>
BL-<Book 2’s Title>-<Book 2’s Author>-<Student Member Name>
BL-<Book N’s Title>-<Book N’s Author>-<Student Member Name>
The highlighted code-fragment should have been before the IF condition. Because of this, the name of the first student member ‘Divyaman’ does not get written into the text file.
The contents that actually get written into the text file are as follows: –
BL-Catcher in the Rye-J D Salinger-Divyaman
BL-The Kite Runner-Khaled Hosseini-Anubhav
BL-A Thousand Splendid Runs-Khaled Hosseini-Jack
Another program reads the data from the text-file (created by the program above) to re-create the students, books and the book-loans. This is achieved by calling the ZCL_LIBRARY=>READ_FROM_FILE method. The source code of this method is as follows: –
This method assumes that the data in the source text file is correct.
Based on this assumption, it: –
- Reads library member names and creates student instances for them.
- It adds the student instances to the instance internal table PIT_STUDENTS.
- It then adds each of the loans to the internal table PIT_BOOK_LOANS.
The side-effect of this is that while we don’t have ‘Divyaman’ as a registered student (in in the internal table PIT_STUDENTS), we do have a book loaned-out in his name.
The problem here is that it was assumed during file read that the data in the source text-file would be correct. The possibility of a book-loan to a student who is not registered was not considered, because it was assumed that this would have been catered to and validated by the method WRITE_TO_FILE.
Ideally, it should have been verified in the READ_FROM_FILE method that each book is loaned to a student that is registered. If not, an exception must be raised.
The modified source-code is as follows: –
In the above source code, the exception raised by the method SEARCH_STUDENT_BY_NAME instead of being handled by a blank handler has been propagated to the calling program since further processing cannot continue if the student has not been found. The calling program, in turn, prints an error message on the screen in the event of an exception.
Throw Early, Catch Later
When an erroneous state is encountered in a program unit, we must throw an exception right at that point because it is here where you will have the most precise information about the error along with the context in which the exception has occurred.
Let’s assume that the SEARCH_STUDENT_BY_NAME method does not raise exceptions and has the following source code: –
In the code above, the method will return a blank student instance in the parameter EX_STUDENT if either the name (IM_NAME) is blank or a student matching that name is not found in the internal table PIT_STUDENTS.
In the above program, the ZCX_STUDENT_BY_NAME exception is raised if a blank student instance is returned by the SEARCH_STUDENT_BY_NAME method. However, please remember that in the case of books which have not been loaned-out, the student name will not be mentioned. Therefore, a blank student instance is a perfectly valid scenario in those cases.
The problem here is that the error occurred in the SEARCH_STUDENT_BY_NAME method while the exception is being raised much later in the READ_FROM_FILE method. By that time an important piece of information is lost – Was the student instance returned as blank because the student name was blank or was it returned as blank because a student with that name wasn’t found. If it’s the former, it is perfectly fine for the READ_FROM_FILE method to continue with further processing. If it’s the latter, it is not possible for the READ_FROM_FILE method to continue processing.
This problem can be fixed by raising the exception early in the SEARCH_STUDENT_BY_NAME method itself. This is shown in the code snippet below: –
In the calling method, we can then introduce exception handling as follows: –
Here, the method propagates the ZCX_STUDENT_NOT_FOUND exception and has a blank handler for the ZCX_BLANK_STUDENT_NAME since we do want to continue processing if the student name against a book is blank.
When it comes to handling exceptions, the calling program might do any of the following: –
- Catch the exception.
- Propagate the exception.
- Catch the exception and raise either a new one or add more information to the exception before raising it again.
This can be seen in the above example also. The SEARCH_STUDENT_BY_NAME method finds the student instance from the PIT_STUDENTS instance internal table. If the ZCX_BLANK_STUDENT_NAME exception is generated, it is handled in the calling method (READ_FROM_FILE).
If the ZCX_STUDENT_NOT_FOUND exception is raised, the READ_FROM_FILE method cannot continue with further processing and the exception is therefore propagated. The calling program catches the exception and decides to display an error message should the exception be raised.
As a concluding remark to this blog in the series, let me emphasize that these are principle/guidelines that need to be considered when thinking about exception handling. Application of these principles without thought could prove to be an over-kill on certain occasions and may result in bloated code.