3

I can't find any substantial example of error handling in Eiffel. I have only found examples that either are trivial, or they completely ignore errors, or they leave error handling to the reader. I am interested in knowing how errors can travel through the call stack in absence of exceptions. For example, I would like to know how an application that sends a network request would inform the user of a network problem that has been detected down the call chain. Something like that.

--

EDIT: I do know the basics of error handling in Eiffel (statuses and exceptions). However, I can't find any substantial example on how applications handle errors via statuses. How are failure statuses chained?

Eleno
  • 2,864
  • 3
  • 33
  • 39
  • 2
    **You do it the same as you may do it in a language that has no exceptions.** Exceptions are reserved for programming errors. e.g. Object has `has_error` set. Program calls a feature with pre-condition (requires) `not has_error`. (Exceptions are used because of a fear that return codes etc could be ignored. Some other languages have the police, that we through exceptions because they can not be ignored, but they can and often are. Eiffel has the police of throwing the exception if you ignore the error.) – ctrl-alt-delor Jul 25 '15 at 21:34

2 Answers2

3

Eiffel advocates use of an object state instead of exceptions. In that case clients may figure out what they expect in case of an error and handle it properly. For example,

has_error: BOOLEAN
        -- Has operation terminated with an error?

error_code: INTEGER
        -- Last error code or `no_error'.

is_closed: BOOLEAN
        -- Is connection closed?

response: detachable RESPONCE
        -- Last response if `not has_error'.

send_request (data: REQUEST)
    require
        is_open: not is_closed
    do
        ...
    ensure
        is_closed: is_closed implies (has_error and not connection.is_open)
        is_successful: not has_error implies attached response
    end

The client can then reason about the state of the supplier object and continue using it in a predictable way:

interface.send_request (...)
if interface.is_closed then
    ... -- The connection is unusable and should be reestablished.
elseif interface.has_error then
    ... -- Inspect `interface.error_code', possibly trying to resend the request.
else
    ... -- Use `interface.response' to continue processing.
end

In presence of exceptions one cannot deduce what should be done in what case except from some documentation. Also, it prevents from using automatic tools that can easily check that the use of response is perfectly valid in the code above.

If an error happens deep down the stack, an exception mechanism can be used with rescue/retry. However it may introduce close coupling between low-level network component and user interface that has nothing to do with details of network failure. In the simplest case, the network class will call {EXCEPTIONS}.raise with an appropriate message. A more specific approach would be to create an object of type EXCEPTION (or a descendant), to set the corresponding message by calling set_description on it, and to raise an exception by calling raise. The user code that will handle the exception may look like.

local
    is_retried: BOOLEAN
    e: EXCEPTIONS
do
    if is_retried then
            -- There was an exception, handle it.
        create e
        if e.developer_exception_name ~ "This error" then
            ... -- Do something.
        elseif e.developer_exception_name ~ "That error" then
            ... -- Do something else.
        else
            ... -- Report yet another error.
        end
    else
        ... -- Some code that may fail with an exception.
    end
rescue
    if not is_retried then
        is_retried := True
        retry
    end
end

EDIT

A specific way to handle nested errors depends on the application design and seems to be irrelevant to the language. Possible alternatives are:

  1. (If exception mechanism is used, not recommended.) After catching a (lower-level) exception and handling it to restore the class invariant, a new exception is raised without cancelling the previous one. Then a query {EXCEPTION}.cause can be (recursively) used to access nested exception objects.

  2. A mechanism similar to the previous one can be used. However instead of creating new objects, a class can delegate a request for details to a lower-level class. For example,

    class A feature
        has_error: BOOLEAN
            do
                Result := nested.has_error
            end
        error: STRING
            do
                Result := "Cannot complete operation X. Reason: " + nested.error
            end
    feature {NONE}
        nested: B
    end
    
    class B feature
        has_error: BOOLEAN
            do
                Result := nested.has_error
            end
         error: STRING
            do
                Result := "Cannot complete operation Y. Reason: " + nested.error
            end
    feature {NONE}
       nested: C
    end
    
  3. Logging facilities can be used. They can differentiate error severity, specify sources, etc.

    class A feature
        do_something
            do
                nested.whatever
                if nested.has_error then
                    log.error ("Cannot complete operation X.")
                end
            end
        has_error: BOOLEAN do Result := nested.has_error end
    feature {NONE}
        nested: B
    end
    
    class B feature
        whatever
            do
                nested.try_something
                if nested.has_error then
                    -- An error has been reported by "nested".
                elseif something_else_goes_wrong then
                    has_inner_error := True
                    log.error ("Something goes wrong.")
               elseif has_minor_issues then
                     log.warning ("Be careful.")
                end
            end
        has_error: BOOLEAN do Result := nested.has_error or has_inner_error end
        has_inner_error: BOOLEAN
                -- Some error that is not one of the errors reported by `nested'.
    feature {NONE}
       nested: C
    end
    
Alexander Kogtenkov
  • 5,770
  • 1
  • 27
  • 35
  • Thanks, but unfortunately this is a "trivial" example because only two routines are involved. I forgot to mention that I know how error handling works in Eiffel. My problem is that I can't find substantial samples that involve more than a couple of routines. For example, in your code, assuming that the error can't be addressed, would `interface` report the error in the same manner (via state)? And would its caller - in turn - report the error (via state)? How are errors chained? For troubleshooting, a lower-level error alone may be obscure, but likewise may be a higher-level error alone. – Eleno Nov 16 '14 at 13:54
  • @Elena, just to be more specific, do you want to propagate error information using exceptions or object state? Also, could you give some more details about the example you have in mind so that the answer could be more specific as well? – Alexander Kogtenkov Nov 16 '14 at 18:55
  • I would like to know how error information is propagated in Eiffel. You said that "Eiffel advocates use of an object state instead of exceptions" and I knew that, but I would like to know how such policy looks in practice beyond trivial examples, when an application must provide detailed diagnostic messages. For this reason, I have suggested an application that uses network facilities (where many things could go wrong), but I won't mind the actual example. Is there an Eiffel application whose sources I could read? – Eleno Nov 16 '14 at 19:33
  • @Elena, there is no universal propagation mechanism - it's up to the application designers to decide how error information is propagated. I'll add a few examples to my answer. As to the sources, you can have a look at [EiffelStudio](https://svn.eiffel.com/eiffelstudio/trunk/Src/Eiffel/) and look for a class `ERROR` and its descendants and clients. – Alexander Kogtenkov Nov 16 '14 at 20:35
  • Exactly. I know that there is no universal propagation mechanism. Therefore I would like to see how experienced application designers have solved the problem. Maybe they have developed some patterns. I will wait for your additional examples before looking at EiffelStudio because I think that it will be too difficult for me to understand it (I am not an Eiffel developer, I am just looking at Eiffel for inspiration). – Eleno Nov 16 '14 at 21:16
  • Hi Elena, in my programs I simply bubble up error messages exactly like C programs would do. So I check if the result of a call succeeded, if not, copy error message, and set is_ok to false. – Berend de Boer Nov 17 '14 at 22:20
  • One addition (see also my other answer): in my http fastcgi programs I can use exceptions as well as I don't recover from them. So raise (developer) exception, this is caught at a high level rescue clause, a 503 message is returned to the client, and the process terminates. This works as I spawn a new process for every call. – Berend de Boer Nov 17 '14 at 22:23
  • @AlexanderKogtenkov: Very interesting additions, thanks. From what I can understand, you don't even have to pass error messages around, because each object in the chain of calls will be able to query the following object about the cause of its failure. – Eleno Nov 18 '14 at 00:55
  • If possible, I would still like to peruse the source code of some applications or libraries that implement the second alternative. Any links? – Eleno Nov 18 '14 at 00:57
  • 1
    @Elena, in [EiffelStudio source code](https://dev.eiffel.com/Source_Code) you can follow the links to get sources of the classes `trunk/Src/Eiffel/eiffel/interface/external_class_c.e` and `trunk/Src/framework/configuration/compiler/interface/conf_consumer_manager.e` and look for `last_error`. The first class creates a new compiler-specific error from a configuration-specific error. Because exceptions are involved (but only to signal errors, not to transfer their meaning), this is a hybrid solution based on approaches 1 and 2 mentioned in the answer. – Alexander Kogtenkov Nov 18 '14 at 16:35
2

In addition to the answer from Alexander, it is sometimes convenient to use exceptions. In Eiffel we don't tend to catch them (usually class invariants have become invalid), but for certain applications you just don't want to deal with errors. If there's an error, you simply stop, and rely on a retry by something outside the program. Examples of libraries who use that approach are ecli and eposix.

Berend de Boer
  • 1,953
  • 20
  • 15