@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