406 lines
16 KiB
Plaintext
406 lines
16 KiB
Plaintext
@database beginner.guide
|
|
|
|
@Master beginner
|
|
|
|
@Width 75
|
|
|
|
|
|
This is the AmigaGuide® file beginner.guide, produced by Makeinfo-1.55 from
|
|
the input file beginner.
|
|
|
|
|
|
@NODE "main" "Exception Handling"
|
|
@Next "Memory.guide/main"
|
|
@Prev "Modules.guide/main"
|
|
@Toc "Contents.guide/main"
|
|
|
|
Exception Handling
|
|
******************
|
|
|
|
Often your program has to check the results of functions and do
|
|
different things if errors have occurred. For instance, if you try to
|
|
open a window (using @{b }OpenW@{ub }), you may get a @{b }NIL@{ub } pointer returned which
|
|
shows that the window could not be opened for some reason. In this case
|
|
you normally can't continue with the program, so you must tidy up and
|
|
terminate. Tidying up can sometimes involve closing windows, screens and
|
|
libraries, so sometimes your error cases can make your program cluttered
|
|
and messy. This is where exceptions come in--an @{fg shine }exception@{fg text } is simply an
|
|
error case, and @{fg shine }exception handling@{fg text } is dealing with error cases. The
|
|
exception handling in E neatly separates error specific code from the real
|
|
code of your program.
|
|
|
|
|
|
@{" Procedures with Exception Handlers " Link "Procedures with Exception Handlers" }
|
|
@{" Raising an Exception " Link "Raising an Exception" }
|
|
@{" Automatic Exceptions " Link "Automatic Exceptions" }
|
|
@{" Raise within an Exception Handler " Link "Raise within an Exception Handler" }
|
|
|
|
|
|
@ENDNODE
|
|
|
|
@NODE "Procedures with Exception Handlers" "Procedures with Exception Handlers"
|
|
@Next "Raising an Exception"
|
|
@Toc "main"
|
|
|
|
Procedures with Exception Handlers
|
|
==================================
|
|
|
|
A procedure with an exception handler looks like this:
|
|
|
|
PROC fred(params...) HANDLE
|
|
/* Main, real code */
|
|
EXCEPT
|
|
/* Error handling code */
|
|
ENDPROC
|
|
|
|
This is very similar to a normal procedure, apart from the @{b }HANDLE@{ub } and
|
|
@{b }EXCEPT@{ub } keywords. The @{b }HANDLE@{ub } keyword means the procedure is going to have
|
|
an exception handler, and the @{b }EXCEPT@{ub } keyword marks the end of the normal
|
|
code and the start of the exception handling code. The procedure works
|
|
just as normal, executing the code in the part before the @{b }EXCEPT@{ub }, but when
|
|
an error happens you can pass control to the exception handler (i.e., the
|
|
code after the @{b }EXCEPT@{ub } is executed).
|
|
|
|
|
|
@ENDNODE
|
|
|
|
@NODE "Raising an Exception" "Raising an Exception"
|
|
@Next "Automatic Exceptions"
|
|
@Prev "Procedures with Exception Handlers"
|
|
@Toc "main"
|
|
|
|
Raising an Exception
|
|
====================
|
|
|
|
When an error occurs (and you want to handle it), you @{fg shine }raise@{fg text } an
|
|
exception using either the @{b }Raise@{ub } or @{b }Throw@{ub } function. You call @{b }Raise@{ub } with a
|
|
number which identifies the kind of error that occurred. The code in the
|
|
exception handler is responsible for decoding the number and then doing
|
|
the appropriate thing. @{b }Throw@{ub } is very similar to @{b }Raise@{ub }, and the following
|
|
description of @{b }Raise@{ub } also applies to @{b }Throw@{ub }. The difference is that @{b }Throw@{ub }
|
|
takes a second argument which can be used to pass extra information to a
|
|
handler (usually a string). The terms `raising' and `throwing' an
|
|
exception can be used interchangeably.
|
|
|
|
When @{b }Raise@{ub } is called it immediately stops the execution of the current
|
|
procedure code and passes control to the exception handler of most recent
|
|
procedure which has a handler (which may be the current procedure). This
|
|
is a bit complicated, but you can stick to raising exceptions and handling
|
|
them in the same procedure, as in the next example:
|
|
|
|
CONST BIG_AMOUNT = 100000
|
|
|
|
ENUM ERR_MEM=1
|
|
|
|
PROC main() HANDLE
|
|
DEF block
|
|
block:=New(BIG_AMOUNT)
|
|
IF block=NIL THEN Raise(ERR_MEM)
|
|
WriteF('Got enough memory\\n')
|
|
EXCEPT
|
|
IF exception=ERR_MEM
|
|
WriteF('Not enough memory\\n')
|
|
ELSE
|
|
WriteF('Unknown exception\\n')
|
|
ENDIF
|
|
ENDPROC
|
|
|
|
This uses an exception handler to print a message saying there wasn't
|
|
enough memory if the call to @{b }New@{ub } returns @{b }NIL@{ub }. The parameter to @{b }Raise@{ub } is
|
|
stored in the special variable @{b }exception@{ub } in the exception handler part of
|
|
the code, so if @{b }Raise@{ub } is called with a number other than @{b }ERR_MEM@{ub } a message
|
|
saying "Unknown exception" will be printed.
|
|
|
|
Try running this program with a really large @{b }BIG_AMOUNT@{ub } constant, so
|
|
that the @{b }New@{ub } can't allocate the memory. Notice that the "Got enough
|
|
memory" is not printed if @{b }Raise@{ub } is called. That's because the execution
|
|
of the normal procedure code stops when @{b }Raise@{ub } is called, and control
|
|
passes to the appropriate exception handler. When the end of the
|
|
exception handler is reached the procedure is finished, and in this case
|
|
the program terminates because the procedure was the @{b }main@{ub } procedure.
|
|
|
|
If @{b }Throw@{ub } is used instead of @{b }Raise@{ub } then, in the handler, the special
|
|
variable @{b }exceptioninfo@{ub } will contain the value of the second parameter.
|
|
This can be used in conjunction with @{b }exception@{ub } to provide the handler with
|
|
more information about the error. Here's the above example re-written to
|
|
use @{b }Throw@{ub }:
|
|
|
|
CONST BIG_AMOUNT = 100000
|
|
|
|
ENUM ERR_MEM=1
|
|
|
|
PROC main() HANDLE
|
|
DEF block
|
|
block:=New(BIG_AMOUNT)
|
|
IF block=NIL THEN Throw(ERR_MEM, 'Not enough memory\\n')
|
|
WriteF('Got enough memory\\n')
|
|
EXCEPT
|
|
IF exception=ERR_MEM
|
|
WriteF(exceptioninfo)
|
|
ELSE
|
|
WriteF('Unknown exception\\n')
|
|
ENDIF
|
|
ENDPROC
|
|
|
|
An enumeration (using @{b }ENUM@{ub }) is a good way of getting different
|
|
constants for various exceptions. It's always a good idea to use
|
|
constants for the parameter to @{b }Raise@{ub } and in the exception handler, because
|
|
it makes everything a lot more readable: @{b }Raise(ERR_MEM)@{ub } is much clearer
|
|
than @{b }Raise(1)@{ub }. The enumeration starts at one because zero is a special
|
|
exception: it usually means that no error occurred. This is useful when
|
|
the handler does the same cleaning up that would normally be done when the
|
|
program terminates successfully. For this reason there is a special form
|
|
of @{b }EXCEPT@{ub } which automatically raises a zero exception when the code in the
|
|
procedure successfully terminates. This is @{b }EXCEPT DO@{ub }, with the @{b }DO@{ub }
|
|
suggesting to the reader that the exception handler is called even if no
|
|
error occurs. Also, the argument to the @{b }Raise@{ub } function defaults to zero
|
|
if it is omitted (see @{"Default Arguments" Link "Procedures.guide/Default Arguments" }).
|
|
|
|
So, what happens if you call @{b }Raise@{ub } in a procedure without an exception
|
|
handler? Well, this is where the real power of the handling mechanism
|
|
comes to light. In this case, control passes to the exception handler of
|
|
the most @{fg shine }recent@{fg text } procedure with a handler. If none are found then the
|
|
program terminates. `Recent' means one of the procedures involved in
|
|
calling your procedure. So, if the procedure @{b }fred@{ub } calls @{b }barney@{ub }, then when
|
|
@{b }barney@{ub } is being executed @{b }fred@{ub } is a recent procedure. Because the @{b }main@{ub }
|
|
procedure is where the program starts it is a recent procedure for every
|
|
other procedure in the program. This means, in practice:
|
|
|
|
@{b }*@{ub } If you define @{b }fred@{ub } to be a procedure with an exception handler then
|
|
any procedures called by @{b }fred@{ub } will have their exceptions handled by
|
|
the handler in @{b }fred@{ub } if they don't have their own handler.
|
|
|
|
@{b }*@{ub } If you define @{b }main@{ub } to be a procedure with an exception handler then
|
|
any exceptions that are raised will always be dealt with by some
|
|
exception handling code (i.e., the handler of @{b }main@{ub } or some other
|
|
procedure).
|
|
|
|
Here's a more complicated example:
|
|
|
|
ENUM FRED=1, BARNEY
|
|
|
|
PROC main()
|
|
WriteF('Hello from main\\n')
|
|
fred()
|
|
barney()
|
|
WriteF('Goodbye from main\\n')
|
|
ENDPROC
|
|
|
|
PROC fred() HANDLE
|
|
WriteF(' Hello from fred\\n')
|
|
Raise(FRED)
|
|
WriteF(' Goodbye from fred\\n')
|
|
EXCEPT
|
|
WriteF(' Handler fred: \\d\\n', exception)
|
|
ENDPROC
|
|
|
|
PROC barney()
|
|
WriteF(' Hello from barney\\n')
|
|
Raise(BARNEY)
|
|
WriteF(' Goodbye from barney\\n')
|
|
ENDPROC
|
|
|
|
When you run this program you get the following output:
|
|
|
|
Hello from main
|
|
Hello from fred
|
|
Handler fred: 1
|
|
Hello from barney
|
|
|
|
This is because the @{b }fred@{ub } procedure is terminated by the @{b }Raise(FRED)@{ub } call,
|
|
and the whole program is terminated by the @{b }Raise(BARNEY)@{ub } call (since
|
|
@{b }barney@{ub } and @{b }main@{ub } do not have handlers).
|
|
|
|
Now try this:
|
|
|
|
ENUM FRED=1, BARNEY
|
|
|
|
PROC main()
|
|
WriteF('Hello from main\\n')
|
|
fred()
|
|
WriteF('Goodbye from main\\n')
|
|
ENDPROC
|
|
|
|
PROC fred() HANDLE
|
|
WriteF(' Hello from fred\\n')
|
|
barney()
|
|
Raise(FRED)
|
|
WriteF(' Goodbye from fred\\n')
|
|
EXCEPT
|
|
WriteF(' Handler fred: \\d\\n', exception)
|
|
ENDPROC
|
|
|
|
PROC barney()
|
|
WriteF(' Hello from barney\\n')
|
|
Raise(BARNEY)
|
|
WriteF(' Goodbye from barney\\n')
|
|
ENDPROC
|
|
|
|
When you run this you get the following output:
|
|
|
|
Hello from main
|
|
Hello from fred
|
|
Hello from barney
|
|
Handler fred: 2
|
|
Goodbye from main
|
|
|
|
Now the @{b }fred@{ub } procedure calls @{b }barney@{ub }, so @{b }main@{ub } and @{b }fred@{ub } are recent
|
|
procedures when @{b }Raise(BARNEY)@{ub } is executed, and therefore the @{b }fred@{ub }
|
|
exception handler is called. When this handler finishes the call to @{b }fred@{ub }
|
|
in @{b }main@{ub } is finished, so the @{b }main@{ub } procedure is completed and we see the
|
|
`Goodbye' message. In the previous program the @{b }Raise(BARNEY)@{ub } call did not
|
|
get handled and the whole program terminated at that point.
|
|
|
|
|
|
@ENDNODE
|
|
|
|
@NODE "Automatic Exceptions" "Automatic Exceptions"
|
|
@Next "Raise within an Exception Handler"
|
|
@Prev "Raising an Exception"
|
|
@Toc "main"
|
|
|
|
Automatic Exceptions
|
|
====================
|
|
|
|
In the previous section we saw an example of raising an exception when
|
|
a call to @{b }New@{ub } returned @{b }NIL@{ub }. We can re-write this example to use
|
|
@{fg shine }automatic@{fg text } exception raising:
|
|
|
|
CONST BIG_AMOUNT = 100000
|
|
|
|
ENUM ERR_MEM=1
|
|
|
|
RAISE ERR_MEM IF New()=NIL
|
|
|
|
PROC main() HANDLE
|
|
DEF block
|
|
block:=New(BIG_AMOUNT)
|
|
WriteF('Got enough memory\\n')
|
|
EXCEPT
|
|
IF exception=ERR_MEM
|
|
WriteF('Not enough memory\\n')
|
|
ELSE
|
|
WriteF('Unknown exception\\n')
|
|
ENDIF
|
|
ENDPROC
|
|
|
|
The only difference is the removal of the @{b }IF@{ub } which checked the value of
|
|
@{b }block@{ub }, and the addition of a @{b }RAISE@{ub } part. This @{b }RAISE@{ub } part means that
|
|
whenever the @{b }New@{ub } function is called in the program, the exception @{b }ERR_MEM@{ub }
|
|
will be raised if it returns @{b }NIL@{ub } (i.e., the exception @{b }ERR_MEM@{ub } is
|
|
automatically raised). This unclutters the program by removing a lot of
|
|
error checking @{b }IF@{ub } statements.
|
|
|
|
The precise form of the @{b }RAISE@{ub } part is:
|
|
|
|
RAISE @{fg shine }exception@{fg text } IF @{fg shine }function@{fg text }() @{fg shine }compare@{fg text } @{fg shine }value@{fg text } ,
|
|
@{fg shine }exception2@{fg text } IF @{fg shine }function2@{fg text }() @{fg shine }compare2@{fg text } @{fg shine }value2@{fg text } ,
|
|
...
|
|
@{fg shine }exceptionN@{fg text } IF @{fg shine }functionN@{fg text }() @{fg shine }compareN@{fg text } @{fg shine }valueN@{fg text }
|
|
|
|
The @{fg shine }exception@{fg text } is a constant (or number) which represents the exception
|
|
to be raised, @{fg shine }function@{fg text } is the E built-in or system function to be
|
|
automatically checked, @{fg shine }value@{fg text } is the return value to be checked against,
|
|
and @{fg shine }compare@{fg text } is the method of checking (i.e., @{b }=@{ub }, @{b }<>@{ub }, @{b }<@{ub }, @{b }<=@{ub }, @{b }>@{ub } or @{b }>=@{ub }).
|
|
This mechanism only exists for built-in or library functions because they
|
|
would otherwise have no way of raising exceptions. The procedures you
|
|
define yourself can, of course, use @{b }Raise@{ub } to raise exceptions in a much
|
|
more flexible way.
|
|
|
|
|
|
@ENDNODE
|
|
|
|
@NODE "Raise within an Exception Handler" "Raise within an Exception Handler"
|
|
@Prev "Automatic Exceptions"
|
|
@Toc "main"
|
|
|
|
@{b }Raise@{ub } within an Exception Handler
|
|
=================================
|
|
|
|
If you call @{b }Raise@{ub } within an exception handler then control passes to
|
|
the next most recent handler. In this way you can write procedures which
|
|
have handlers that perform local tidying up. By using @{b }Raise@{ub } at the end of
|
|
the handler code you can invoke the next layer of tidying up.
|
|
|
|
As an example we'll use the Amiga system functions @{b }AllocMem@{ub } and @{b }FreeMem@{ub }
|
|
which are like the built-in function @{b }New@{ub } and @{b }Dispose@{ub }, but the memory
|
|
allocated by @{b }AllocMem@{ub } @{i }must@{ui } be deallocated (using @{b }FreeMem@{ub }) when it's
|
|
finished with, before the end of the program.
|
|
|
|
CONST SMALL=100, BIG=123456789
|
|
|
|
ENUM ERR_MEM=1
|
|
|
|
RAISE ERR_MEM IF AllocMem()=NIL
|
|
|
|
PROC main()
|
|
allocate()
|
|
ENDPROC
|
|
|
|
PROC allocate() HANDLE
|
|
DEF mem=NIL
|
|
mem:=AllocMem(SMALL, 0)
|
|
morealloc()
|
|
FreeMem(mem, SMALL)
|
|
EXCEPT
|
|
IF mem THEN FreeMem(mem, SMALL)
|
|
WriteF('Handler: deallocating "allocate" local memory\\n')
|
|
ENDPROC
|
|
|
|
PROC morealloc() HANDLE
|
|
DEF more=NIL, andmore=NIL
|
|
more:=AllocMem(SMALL, 0)
|
|
andmore:=AllocMem(BIG, 0)
|
|
WriteF('Allocated all the memory!\\n')
|
|
FreeMem(andmore, BIG)
|
|
FreeMem(more, SMALL)
|
|
EXCEPT
|
|
IF andmore THEN FreeMem(andmore, BIG)
|
|
IF more THEN FreeMem(more, SMALL)
|
|
WriteF('Handler: deallocating "morealloc" local memory\\n')
|
|
Raise(ERR_MEM)
|
|
ENDPROC
|
|
|
|
The calls to @{b }AllocMem@{ub } are automatically checked, and if @{b }NIL@{ub } is returned
|
|
the exception @{b }ERR_MEM@{ub } is raised. The handler in the @{b }allocate@{ub } procedure
|
|
checks to see if it needs to free the memory pointed to by @{b }mem@{ub }, and the
|
|
handler in the @{b }morealloc@{ub } checks @{b }andmore@{ub } and @{b }more@{ub }. At the end of the
|
|
@{b }morealloc@{ub } handler is the call @{b }Raise(ERR_MEM)@{ub }. This passes control to the
|
|
exception handler of the @{b }allocate@{ub } procedure, since @{b }allocate@{ub } called
|
|
@{b }morealloc@{ub }.
|
|
|
|
There's a couple of subtle points to notice about this example.
|
|
Firstly, the memory variables are all initialised to @{b }NIL@{ub }. This is because
|
|
the automatic exception raising on @{b }AllocMem@{ub } will result in the variables
|
|
not being assigned if the call returns @{b }NIL@{ub } (i.e., the exception is raised
|
|
before the assignment takes place), and the handler needs them to be @{b }NIL@{ub }
|
|
if @{b }AllocMem@{ub } fails. Of course, if @{b }AllocMem@{ub } does not return @{b }NIL@{ub } the
|
|
assignments work as normal.
|
|
|
|
Secondly, the @{b }IF@{ub } statements in the handlers check the memory pointer
|
|
variables do not contain @{b }NIL@{ub } by using their values as truth values. Since
|
|
@{b }NIL@{ub } is actually zero, a non-@{b }NIL@{ub } pointer will be non-zero, i.e., true in
|
|
the @{b }IF@{ub } check. This shorthand is often used, and so you should be aware of
|
|
it.
|
|
|
|
It is quite common that an exception handler will want to raise the
|
|
same exception after it has done its processing. The function @{b }ReThrow@{ub }
|
|
(which has no arguments) can be used for this purpose. It will re-raise
|
|
the exception, but only if the exception is not zero (since this special
|
|
value means that no error occurred). If the exception is zero then this
|
|
function has no effect. In fact, the following code fragments (within a
|
|
handler) are equivalent:
|
|
|
|
ReThrow()
|
|
|
|
IF exception THEN Throw(exception, exceptioninfo)
|
|
|
|
There are two examples, in Part Three, of how to use an exception
|
|
handler to make a program more readable: one deals with using data files
|
|
(see @{"String Handling and I-O" Link "Examples.guide/String Handling and I-O" }) and the other deals with opening screens and
|
|
windows (see @{"Screens" Link "Examples.guide/Screens" }).
|
|
|
|
|
|
@ENDNODE
|
|
|