Take advantage of Eiffel Exceptions as Objects

by Tao Feng (modified: 2014 Nov 19)

I was asked what the benefit of Exceptions as Objects (EAO) was in Eiffel. My short answer was flexibility of design. I knew that this answer was far from sufficient. Now I take the time my machine is busy running tests to think about it deeper and write it down. Hopefully people who are trying using it or starting to learn this area of Eiffel may better understand the mechanism and its benefits. What I am talking maybe far from sufficient too and don't blame me I know I am not a good English writer.

Before we go into the subject EAO, I would talk about some basics of exception handling and the previous way of exception handling in Eiffel.

Real applications normally have the situations they run into exceptional context in which they don't have enough information to deal with such exceptional cases. Hence they have to do something to flag the rest of the world possibly with the interesting information of exceptional contexts. Informing in applications implies handling, though exception handling could be nothing. Without good exception handling mechanism, coding to handle various exceptional cases is a nightmare and the code is almost unreadable. A good exception handling mechanism perfectly decouples the code for normal application processes and that for exception handling. Thank Eiffel for its born readability. The built-in rescue-retry exception handing mechanism has been good enough to achieve the decoupling. Codes are well arranged and of really nice readability. Consider the following piece of code: Example1 class APP create make feature {NONE} -- Initialize make do normal_process rescue handle_my_exception retry end feature {NONE} -- Normal logic of the program normal_process do if not is_initialized then exceptions.raise ("Not initialized.") else -- do something else. end end is_initialized: BOOLEAN feature {NONE} -- Exception handling handle_my_exception -- Code to handle exceptions do is_initialized := True end feature {NONE} exceptions: EXCEPTIONS once create Result end end

In Example1, an exception is raised in normal_process' with the tag of "Not initialized." (It is call "message" in EAO), when not is_initialized'. For normal_process', this is an exceptional context, it doesn't know how to fix this, so raises it to higher level that knows better the context and how to handle the not is_initialized' exception. Now rescue of make' handles the exception. is_initialized' is simply set with True. One may argue that normal_process' here knows enough information to handle the case not is_initialized'. But the reality is much more complicated, where is_initialized' may not be a field or the normal_process' is a client of a library in which exception is raised.

In Eiffel, once an exception is raised, the calling routine aborts, the runtime backtracks the call stack to find the nearest rescue and step into the rescue. If no rescue is found, backtracking reaches the bottom of the call stack where a execution vector with a hidden rescue was pushed. The hidden rescue of course is executed to do necessary clean-ups and also to print the exception call stack. That 's what people usually see in console if an Eiffel application crashes.

Exceptions as Objects (EAO)

Now EAO is something new, but keep in mind the rescue-retry mechanism is not obsolete. The way of rescuing and internally backtracking don't change at all. The ultimate change is the ability of information encapsulation from which we will benefit a lot. I will talk about it later. The new mechanism, as the name implies, is based on objects. In this article, http://dev.eiffel.com/Exceptions_as_Objects , one can see the exception hierarchy, interfaces and some other topics. EXCEPTION is the top most class in the hierarchy, which means all exceptions derive from it. {EXCEPTION}.raise raises the exception object. In previous way of EXCEPTIONS class, take the Example1 as an instance, the exception raised was only a code `developer_exception' defined in the class EXCEP_CONST and the tag, a string of "Not initialized.". Raising exceptions in Eiffel was almost fully governed by the class EXCEPTIONS. Apart from this class, nothing exception related had to do with objects, it was code based exception handling.

Example2: class APP create make feature {NONE} -- Initialize make do normal_process rescue handle_my_exception ((create {EXCEPTION_MANAGER}).last_exception) retry end feature {NONE} -- Normal logic of the program normal_process local l_exception: NON_INITIALIZED_EXCEPTION do if not is_initialized then create l_exception l_exception.set_message ("Not initialized.") l_exception.raise else -- do something else. end end is_initialized: BOOLEAN feature {NONE} -- Exception handling handle_my_exception (e: EXCEPTION) -- Code to handle exceptions do is_initialized := True end end
class NON_INITIALIZED_EXCEPTION inherit DEVELOPER_EXCEPTION feature end
In Example2, a developer exception is defined as class NON_INITIALIZED_EXCEPTION. In the exceptional context not is_initialized' in normal_process', the developer defined exception is created by developer, filled with exceptional information "Not initialized" and raised by calling raise'. Once raise' is call, the following code, if any, in that routine wouldn't be called. The nearest rescue is hit in make'. The exception object, containing exceptional information, can be accessed by calling {EXCEPTION_MANAGER}.last_exception. With the exception object, handle_my_exception', who knows better how to handle the exceptional context, is able to do more if needed.

From the whole system perspective, what is the EAO doing? The answer is simple. It provides a universal way to store and access exceptional context information wrapped as objects, and the scope is from deep into the runtime to very top of the application. Example2 demonstrates how developer exceptions are raised and get handled. We can think about what it is the situation if NON_INITIALIZED_EXCEPTION, is_initialized' and the call normal_process' is defined in a library. We find that it is the same thing, but better explains how one benefits doing the library when he knows nothing about how the situation should be handled by its client application. Responsibility is brought in here and very clear that who is responsible to collect exceptional information and who is responsible to handle it with that information. Maybe one is more interested in system raised exceptions. From my point of view, there is nothing really special compared with those raised by developers. The difference is that one particular Eiffel compiler with its runtime might have different implementation, and those system raised exceptions are created and raised by the runtime or by code generated from the compiler. System raised exceptions could be raised anywhere in the code, in most cases, they implies bugs. Finally, about system raised exceptions, some of them, such as operating system failures, are actually a handler in the runtime who does mappings between exceptions raised from the OS and the language exceptions.

As the idea of exception handling is introduced simply, hopefully not too simple, I can go ahead the benefits of EAO as I can see.

Benefits

Encapsulation

As stated earlier, compared with the previous implementation of exception handling in Eiffel, the ultimate enhancement of EAO is information encapsulation. People benefit from object-oriented way of this good manner of information encapsulation. Code is clear, when all exceptional information is encapsulated in the exception object which is directly accessible later through EAO mechanism when handling. Let's have a look at complexer example Example3: class MY_APP feature read_data local l_reader: DB_READER do create l_reader.make (new_connection_string) l_reader.read rescue if (ex: !CONNECTION_FAILURE)exception_manager.last_exception then print ("ex.message" + "%N") print ("Name: " + ex.name + "%N") print ("Domain: " + ex.domain + "%N") print ("password: " + ex.passwd + "%N") end retry end new_connection_string: STRING do -- Code to get new connection string. end exception_manager: EXCEPTION_MANAGER once create Result end end

   In library "database":

class DB_READER create make feature {NONE} -- Initialization make (a_str: like connection_string) do connection_string := a_str end feature -- Element change set_connection_string (a_str: like connection_string) do connection_string := a_str end feature -- Actions read do try_connect -- Read action ommitted. end feature {NONE} try_connect is local l_domain, l_name, l_passwd: STRING l_exception: CONNECTION_FAILURE do l_name = extract_name (connection_string) l_domain = extract_domain (connection_string) l_passwd = extract_passwd (connection_string) connect (l_domain, l_name, l_passwd) if not is_connected then create l_exception.make (l_name, l_domain, l_passwd, "Connection failed!") l_exception.raise end end connect (domain, name, passwd: STRING): BOOLEAN is do -- Connect end is_connected: BOOLEAN connection_string: STRING end
class CONNECTION_FAILURE inherit DEVELOPER_EXCEPTION create make feature {NONE} -- Initializaton make (a_domain, a_name, a_passwd, a_message: STRING) do doamin := a_domain name := a_name passwd := a_passwd set_message (a_message) end feature -- Access domain, name, passwd: STRING end

Of course Example3 is code from a real system, there are a lot more things we need to do within real systems. And I don't add any contract in this piece of code, since I want to focus more on the exception handling. In this example, we see domain, name and passwd as exceptional context which is saved in the exception object CONNECTION_FAILURE. In this exceptional context of the library "database", there is no information how it should proceed, so the exception is raised to upper level. We can think about how we did with old implementation of exception handling. What we supposed to do was simply call `raise ("Connection failed!")'. At the client side in rescue, only information of the exception code and message was available. People could have their own way work around. He could put information like domain, name and passwd in the DB_READER as queries or had his own class to wrap this information and make it available somewhere accessible in the library. But as you see, those ways working around were awkard, since for clients, it was difficult to know where the useful information was. EAO now does the good job, it is a standard way for the library developers to encapsulate exceptional information and for the clients to get the information.

Extendibility

This is what we benefit from object-oriented method. Inheritance, polymorphism and so on. The previous way of exception handling only have one code for developer exceptions. That's far from enough. Now one can use the type system to define it own exceptions as many as he may want, as long as it inherits from DEVELOPER_EXCEPTION. Still use Example3, in the "database" library, there can be many kind of connection failures like DB2_CONNECTION_FAILURE, ORACLE_CONNECTION_FAILURE and so on. They all inherit from CONNECTION_FAILURE. At client side, there is no particular change should be done if details are not that important. One may also notice that with this extension, there is no need for the client to know each kind of exceptions. But in previous way, one may have to write code like this: foo do ... rescue if db2_connection_failure then elseif oracle_connection_failure then end end
Or somewhere has a query like this. This is an example of better extendibility people benefit from inheritance. I believe one can easily have his own example of better extendibility from polymorphism.

Maintenance

With object-oriented method, it has been proved that code are can be easier maintained. I don't think I need to talk about this, there are a bunch of books about it.

Better Debugging

ISE debugger has supported EAO, which means that it will be easier to debug when with the caught exception object in the debugger. Since exception objects are proper places to collect informative exception context. With a single object view, one can get useful information like the trace and, more important, the information filled by the coder who intended to expose it.

I am stopping here. I am too lazy to explore more. But I will extend it if I find more.