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:
(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.
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
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