About exceptions today. First - if you're new to exceptions, probably 'exception' means to you 'an ugly error message, full of Access denied, memory under 0x462F3ED4 cannot be read stuff, contact the sucker who sold you this product.' Time to change this way of thinking!
Why the hell they invented exceptions
Imagine a complex system, let's say, a system to operate some machine in a factory. The system is divided into layers, that is (for example, in order) direct motor control, motor control proxy, motor controller, control logic, user commands layer, several other layers, the machine operation layer, several more layers, and finally the user interface, where user wants the machine to do something.
So you can imagine, that when user presses a button, then a function from user interface layer calls a function from the next layer, that calls a function from the machine operation logic layer, that... that finally sends a signal to the inverter to run the motor. And now imagine that the motor is completely broken, burnt, stalled or stolen. It's a critical error, so user must be immediately informed about this, and no further action of any of the layers is needed.
The naïve solution: each of the functions should return an integer with error code, and each function, when calling the next one in the chain, should check its return value, and if it signalises an error, it should return immediately with the same error code, until the last function (the first that was called) gets the message. It's a good solution, meaning that it can work. But it's a very bad solution:
- The code becomes ugly and long.
- Functions cannot return any other value because they already return the error code, and it complicates even further.
- If a function has to do something no matter if it succeeded or not (like close a file or release resources), and it complicates even further than further.
- The code becomes ugly and long.
- The code becomes ugly and long.
Have a look at an example:
If you're not completely blind, you see the ugliness of this code. (Of course in Ruby we could do some improvements, but imagine it is C. And remember there is a lot more functions that call each the next one.)
Any idea for a solution? Well, the best one is, if the most inner function could inform the most outer function that something's completely wrong. But how to do it? And what if we don't want to inform the most outer function, for example because the problem is not so critical, and can be resolved by the program?
Let's finally present the solution with exceptions, or
Notice any differences? Let's explain what happened. First, we declared our error class, descendant of the standard error class in Ruby,
Now, when an error occurs, we
But here we don't want the program to exit. So we make a trap. A trap is the
What we do in the
As you see, there's one more trap, inside
As you see, the exceptions are very useful, simple, elegant, powerful and in general good. Use them!! Learn them, use them, think about them, or else you're not a programmer for me.
Final remarks about exceptions
The two following examples are equivalent:
And the second, looks a bit nicer for me:
Another remark.
This is useless, because our
Also, as you see, the
The last remark. If a function in the calling chain thinks it cannot serve the exception, but thinks that it could add some additional info to the error, it can
In this way, the exception gets partially served, and then reraised so that it does not get cancelled at this point.
Use it, use it, use it!
Why the hell they invented exceptions
Imagine a complex system, let's say, a system to operate some machine in a factory. The system is divided into layers, that is (for example, in order) direct motor control, motor control proxy, motor controller, control logic, user commands layer, several other layers, the machine operation layer, several more layers, and finally the user interface, where user wants the machine to do something.
So you can imagine, that when user presses a button, then a function from user interface layer calls a function from the next layer, that calls a function from the machine operation logic layer, that... that finally sends a signal to the inverter to run the motor. And now imagine that the motor is completely broken, burnt, stalled or stolen. It's a critical error, so user must be immediately informed about this, and no further action of any of the layers is needed.
The naïve solution: each of the functions should return an integer with error code, and each function, when calling the next one in the chain, should check its return value, and if it signalises an error, it should return immediately with the same error code, until the last function (the first that was called) gets the message. It's a good solution, meaning that it can work. But it's a very bad solution:
- The code becomes ugly and long.
- Functions cannot return any other value because they already return the error code, and it complicates even further.
- If a function has to do something no matter if it succeeded or not (like close a file or release resources), and it complicates even further than further.
- The code becomes ugly and long.
- The code becomes ugly and long.
Have a look at an example:
def prepare()
if motor_in_bad_mood?
return 225 # the error code for the problem
end
inverter.init()
return 0
end
def do_it()
if ufo_stole_the_cables?
return 843 # the error code
end
inverter.operate()
# further operation
return 0
end
def almost_do_it()
f=allocate_resources()
if (ret=prepare())!=0
f.free_resources()
return ret
end
if (ret=do_it())!=0
f.free_resources()
return ret
end
f.free_resources()
return 0
end
def user_says_do_it()
if (ret=almost_do_it())!=0
puts "The operation returned the error code! (#{ret})"
end
end
Any idea for a solution? Well, the best one is, if the most inner function could inform the most outer function that something's completely wrong. But how to do it? And what if we don't want to inform the most outer function, for example because the problem is not so critical, and can be resolved by the program?
Let's finally present the solution with exceptions, or
Exceptions
, as we should call them now.class CriticalMotorError < RuntimeError
end
def prepare()
if motor_in_bad_mood?
raise CriticalMotorError,225
end
inverter.init()
end
def do_it()
if ufo_stole_the_cables?
raise CriticalMotorError,843
end
inverter.operate()
# further operation
end
def almost_do_it()
f=allocate_resources()
begin
prepare()
do_it()
ensure
f.free_resources()
end
end
def user_says_do_it()
begin
almost_do_it()
rescue CriticalMotorError => e
puts "The operation returned the error code! (#{e.message})"
rescue RuntimeException => e
puts "Something even worse happened: #{e.message}."
end
end
RuntimeError
(which is a descendant of Exception
used for most error that happen during program runtime). Our exception class doesn't do anything special, it just inherits from the ancestors.Now, when an error occurs, we
raise
an exception (in other languages the keyword throw is often used here). That means that we create a new instance of our exception, pass it an argument (exception simply stores it as an error message and does nothing with it), and then raise
thus created exception. Raising means that all further actions in the current function are aborted, and the control returns to the higher function, but here also all actions are aborted, and the function exits immediately, and all functions in the chain exit in a row. If we just threw an exception and then didn't take care of it, it would exit all the functions, and finally also exit the program with an error message (try to type this in irb: def x;0/0;end;def y;x;end;def z;y;end;zto see this behaviour in action).
But here we don't want the program to exit. So we make a trap. A trap is the
begin
and end
in user_says_do_it
, and the rescue
. Basically, if things that are called after begin
throw an exception, and the exception is mentioned in one of the rescue
clauses, then the rest of the block after begin
is skipped and the control goes to the right rescue
clause (the first that mentions the actual class of the exception), and then resumes after end
, and continues to run the program normally (the exception is cancelled once the control enters the rescue
clause, and the exit-immediately madness stops).What we do in the
rescue
clause, we print an error message, including the message (error code) that we read from within the object e
which is our exception. Simple, elegant, painless.As you see, there's one more trap, inside
almost_do_it
. It also detects exceptions raised from the block, but it doesn't rescue them, it just isn't interested in what really happened, or it decides it doesn't have power to serve any errors correctly, so it just lets the exceptions pass through, also skipping the rest of the block (so if the exception was raised by prepare
, do_it
won't even try to execute. But here's the trick: the ensure
block gets executed no matter what happened inside the begin
block. It executes both when the block completes normally, and when it is interrupted by an exception, but ensure
doesn't cancel the exception, it just stops for a moment to do what it has to do, and goes on with the unrolling madness.As you see, the exceptions are very useful, simple, elegant, powerful and in general good. Use them!! Learn them, use them, think about them, or else you're not a programmer for me.
Final remarks about exceptions
The two following examples are equivalent:
begin
try_something()
puts "Success." # of course this line is executed only if no exception was raised
rescue SomeError => e
error()
# possibly more rescue clauses
end
begin
try_something()
rescue SomeError => e
error()
# possibly more rescue clauses
else
puts "Success."
end
Another remark.
begin
# ...
rescue RuntimeError
# ...
rescue CriticalMotorError
# ...
end
CriticalMotorError
is also a RuntimeError
, so the first rescue
will be triggered, and always when a rescue
is triggered, all following rescue
s are skipped and not even checked.Also, as you see, the
=> exception_varcan be omitted. The exception class name can be omitted also, and it defaults to
StandardError
. For standard exception classes, check QuickRef, the part Exceptions, Catch, and Throw (catch
and throw
are not so good tools, though).The last remark. If a function in the calling chain thinks it cannot serve the exception, but thinks that it could add some additional info to the error, it can
rescue
it and either throw a new error with some more data (possibly of some other class than the original exception), or it can do something like this:begin
# ...
rescue SomeException => e
puts "The exception was rescued: #{e.message}"
puts some_additional_info
raise e
end
Use it, use it, use it!
No comments:
Post a Comment