TweetFollow Us on Twitter

Error Handler Pascal

Volume Number: 15 (1999)
Issue Number: 9
Column Tag: Programming Techniques

A Simple Error Handler in Pascal

by Jim Phillips

Introduction

One of the big differences between code you write for yourself and code you write for others is the quality of the runtime error handling. Your users will be much happier if you handle runtime errors gracefully. Gracefully means not destroying their data and preventing system crashes when errors occur that are nobody's fault. Your users have no recourse when your program misbehaves. They cannot debug or fix the code as you can. Simply put, part of being professional is handling runtime errors.

Unfortunately, writing error handling code is one of the more boring and tedious tasks that application programmers do. It therefore pays to simplify the writing of error handling code as much as possible by capturing repeated code in a separate module and reusing it throughout your application.

This has two additional benefits. First, it gives you an opportunity to put your application's mark on the error handling rather than defer to the system, compiler, or, possibly, third-party libraries. Second, it eliminates the need for a separate console window for debug messages during development. If you have gone to the trouble to create an attractive interface for displaying errors to the end user, it's surely good enough for you.

Typically, you may have to do four things to handle an error.

  • Check for the error.
  • Report the error to the end user.
  • Clean up.
  • Exit from the failed procedure or function.

The last two items may have to be repeated for each procedure or function in a chain of nested calls.

This article describes a module to organize and simplify the writing of error handling code for Macintosh applications. Since you have the source, you can easily adapt it to your application.

The source is presented in Apple's version of Pascal. However, the module can be implemented in C or C++. A version in C++ is available at <ftp://ftp.mactech.com>.

Goals for the Error Handler

This section describes five goals for the error handler module.

First, the work horse error handling procedures should be short and easy to use consistent with performance and reliability. If these procedures are not easy to use, then they probably won't be.

Second, it should be easy for a client to write the error handling code without introducing programming errors. It is very annoying when a low frequency problem occurs and the user gets the wrong error message, or worse, no error message. Also the error handling code that executes after an error is detected should not itself cause crashes or destroy the user's data. This would be adding insult to injury.

Third, execution of the shipping code should be efficient in the absence of detected errors. However, it is not so important for the code that displays the error and cleans up the mess to be efficient. It's much more important that this code is correct and that it succeeds.

Fourth, the error handling module should be as complete as possible. We should have a convenient way to handle non-fatal, recoverable errors as well as fatal programming errors (bugs).

Memory mismanagement is a common type of error in programming languages without garbage collection. For this reason, the error handler should not try to allocate memory after an error is detected. This can be avoided by allocating and locking all memory required by the error handler early in the startup process.

In summary, our error handler module should have the following characteristics:

  • Implementation of error handling should be easy for the client.
  • Using the error handler should not be error prone.
  • Normal successful execution should be efficient.
  • It should handle everything from non-fatal errors to programming errors.
  • Error reporting should be safe even when memory is low.

Some of these goals are conflicting, so compromise will be necessary.

Handling Programming Errors in the Shipping Code

The standard thinking on debugging is that there should be two versions of your application code: the debug version and the shipping version. The debug version typically uses assertions and specialized testing code controlled by compiler directives. This extra debug code handles errors that would be fatal if they occurred in the shipping version. Errors that are nobody's fault, such as running out of memory, are handled gracefully whether they are fatal or nonfatal and this error handling is normally part of the shipping code. When all the bugs are found, the assertions and specialized testing code are removed for the shipping version. This also removes all overhead associated with the debug code, leaving only the no-fault error handling code. We have our cake and eat it too.

Or do we? As a developer, does anything bother you about this description? How about the part where we find all the bugs? And what is the consequence of removing all of our bug detection apparatus and leaving the end user to deal with bugs that escape to the shipping version?

Not everybody drops the ball in this respect. Occasionally, you see examples of programming errors that are handled in the shipping code. Here is one.

While using Symantec's C++ Compiler, I got the following error message: internal error 'file name' line number. The explanation for this error message in the Symantec C++ Compiler Guide is:

"This indicates a defect in the Symantec C++ compiler. Please contact Symantec technical support with details of this problem, including the filename and line number reported."

This is reporting a possible programming error in the compiler code. Not handling this error might have caused a crash and would have made it nearly impossible to find the bug. I followed up and reported this error and to my knowledge they fixed it.

Under "Error Message Types" in the Symantec Compiler manual there is further information about internal errors: "An assertion failure within the compiler generates this type of error ...". This got me thinking about intentionally leaving assertion-like statements in the shipping code.

The Trouble with Assertions

The reason we use assertions is to find bugs during development. Assertions are not supposed to be used to handle errors because they will be removed from the shipping code. Since they will be removed, we can use them freely without worrying about performance.

On the plus side, assertions are probably the simplest way to state an error condition. They completely hide the reporting, cleanup, and exiting steps of error handling. As such, they are very easy to use which means that they are more likely to be used.

However, assertions have two important problems: side effects and no protection in the shipping code. The first can mask bugs in the shipping code and the second makes it very hard to find the difficult bugs that escape the development process.

Avoiding side effects requires care on the part of the programmer, both in the implementation of Assert and in the use of Assert. Steve Maguire in "Writing Solid Code" describes why you probably do not want to write your own assertions. You have to be very careful that memory is not allocated or moved when an assertion is used. Otherwise, the shipping code executes differently from the debug code. For this reason, assertions in C/C++ are implemented as macros rather than functions. Even so, there is no way to have identical memory usage because the code itself is larger with assertions turned on.

When you use assertions, you have to avoid putting function calls as an argument to the assertion. When the assertion is removed for shipping these functions will not be executed, possibly introducing undetected bugs into the shipping code. For more information on correct use of assertions see Peter Lewis's excellent MacTech article "Using Assert()" (Lewis, 1997) and Steve Maguire's book "Writing Solid Code" (Maguire, 1993).

The worst thing about assertions is that they don't guarantee that your shipping code has no bugs. You can look for bugs, try to prevent them, and test for them, but you can't prove that you found them all. Therefore, it is possible for bugs to escape to the shipping code. And they do, don't they. Furthermore, these "shipping bugs" are more likely to be obscure and hard to find because they got through your careful development process. And since you've removed the assertions that would have flagged these bugs, ironically, the program is more likely to crash in the user's hands.

What can we do about shipping bugs? One approach is to leave a few assertion-like checks in the shipping code. They have the advantage that, by definition, they can't introduce side effects into the shipping code. And they protect the end user and may help you find bugs in the shipping code.

Error Checking Code Performance

The most direct way to accomplish the four error handling tasks is to write a procedure that takes three arguments: the boolean expression to be checked, the error message, and a cleanup procedure to be executed. If we consider performance in the normal successful case, only the boolean expression will be checked. Unfortunately, the overhead of a procedure call dwarfs the time required to check a boolean expression.

For example, consider the following two code snippets:

(a) HandleErrorIf(error <> noErr, message, CleanUpAndExit);

(b) if (error <> noErr) then
		HandleError(message, CleanUpAndExit);

Using CodeWarrior with debugging and optimization off, Code snippet (a) runs about 3 times slower than code snippet (b) when the string is passed by reference and about 15 times slower when the string is passed by value. This ratio will vary depending on the compiler and the language, but it is always significant because of the overhead of the procedure call. The down side of code snippet (b) is that it requires a little more text, so it is a little less convenient. However, in my judgement, the performance hit is just too great when there is no error. So we will use code snippet (b) as our model.

That being said, if the error check is a simple boolean expression, then our handling of error conditions is extremely cheap when no error occurs. The compiled code to check the error condition is at most a few instructions and it is, in any case, the minimum required to detect an error. There is just no excuse not to check error codes, for example.

Note that we can easily afford the procedure overhead after an error has occurred. Since the error handling procedures will be executed only a few times at most, the extra overhead of the procedure call will only take a fraction of a second.

You can have your cake and eat it too, if you are comfortable using macros. CodeWarrior lets you do macros in Pascal. C/C++ programmers will not hesitate, of course! Here is how you would implement the macro in Pascal:

{$DEFINEC ProgramErrorIf(condition, message, CleanUpAndExit)

	if (condition) then ProgramError(message, CleanUpAndExit)}

You can see that you are not actually saving that many keystrokes!

Workhorse Procedures: HandleError, LogError, and ProgramError

The prototypes for these procedures in increasing order of severity are:

procedure HandleError(message : Str255;
		procedure CleanUpAndExit);
procedure LogError(message : Str255;
		procedure CleanUpAndExit);
procedure ProgramError(message, procName, unitName : Str255);

Each of these three functions performs the last three tasks of handling an error listed in the introduction. The first task, checking the error condition, is always handled directly in the code for performance reasons.

The first procedure, HandleError, is intended for the end-user. It displays a modal "stop" alert dialog, cleans up any processes that are partially completed, sets any error codes, and exits from the failed procedure. The error message should contain what went wrong, why it went wrong, and suggestions for correcting the problem. It should be clear, brief, and complete. It should give information in terms that the average user can understand. There should be just one such dialog per error. Paige Parson's article "Guidelines for Effective Alerts" (Parsons, 1995) gives lots of great advice about the content of such dialogs.

The messages shown by HandleError should be easily localizable, so we will use string resources. We will also implicitly take advantage of Toolbox text utilities that will display messages correctly in many different languages.

From a programming perspective, there may be a chain of calls resulting in a failure in some low-level procedure. To recover, you need to cleanup and return from each procedure until you get back to the main event loop. There is usually an ideal procedure from which to show the error dialog, and this is not necessarily in the procedure where the error occurred. If you show the dialog at too low a level, your message is apt to be too technical and far removed from what the user was doing. If you show the message at too high a level, your message may be too vague; you may have lost critical details about the nature of the error. Choosing where is a judgement call, but there should be only one error dialog shown.

The second procedure, LogError, is intended for the developer or sophisticated user. It opens an error log file, writes the error message, closes the file, cleans up, sets error codes, and exits from the failed procedure. The number of error messages written to the error log file is limited and the file is rewritten each time the program is run. This prevents the accumulation of "garbage" files, either in the form of one large file or many small files, that the user may not even know are accumulating. The messages can be technical and in the developer's native language. They can report system errors or anything that the developer might find useful for debugging.

LogError is useful when an error is discovered deep in the bowels of the program. It is not appropriate to call HandleError because the program is at too low a level. But it is sometimes nice to know exactly what the first error was. LogError lets you record the error without interrupting the user with information that may not be useful to them, or worse, frighten them.

The third procedure, ProgramError, is intended strictly for the developer. You only call this procedure if a serious error has been detected and it is too dangerous for the program to continue or even clean up. The most important thing to do in this case is to report the error. ProgramError displays an alert dialog that describes the error, its location in the code, and then exits the program.

This scheme relies on the user to forward error information to the developer as in the Symantec "internal error". Perhaps a reward should be offered to users for help in reporting bugs. An announcement to this effect could be included in the alert dialog.

You should use ProgramError to at least check arguments that come from outside the module containing the procedure or function. The procedure or function cannot be expected to give correct results if its inputs are wrong. In other words, the bug lies outside the module; it is a client error. Now as the programmer of the module, you have chosen its scope to be intellectually manageable. You want to be able to debug the module in isolation. But if you do not check its inputs, you allow an upstream error to propagate and it may not be caught by other sanity checks further downstream in your procedure or function. This couples modules together, violates your own decomposition, and makes it so you can't debug the module in isolation.

Sometimes it is too expensive to check inputs to your module in the shipping code. In that case, at least do inexpensive sanity checks. It is very important to start off on the right foot.

Using ProgramError to check internal constraints of your module is really looking for bugs within the module. Here, there will be a tradeoff between the cost of checking in the absence of error and the value of catching bugs in the shipping code. In some cases, the cost of checking can ruin the performance of an algorithm. You should use ProgramError in combination with assertions and/or specialized debugging code controlled by compiler directives.

Using the Error Handler: MyApplication Example

The error handler code makes use of Pascal's nested procedures and the standard "exit" procedure. For each procedure or function where HandleError or LogError may be called, the programmer writes a nested procedure that cleans up anything that was done before the error was detected. This nested cleanup procedure then sets any return results and exits from the outer procedure.

For example, let's say that your application opens a document file and loads the data into a buffer. We'll simulate this with a function that allocates two handles of different sizes. The interface for our utilities unit (MyUtilities.p) defines the file data structure that contains the file spec and two buffers and the open file prototype.

unit MyUtilities;

interface

	type
		tMyFile = record
			smallBuffer: Handle;
			largeBuffer: Handle;
			fileSpec: FSSpec;
		end;

	function MyOpenFile (fileName: Str255;
			var fileData: tMyFile): OSErr;

Now we implement the MyUtilities unit.

First, we import the ErrorHandler unit, declare private constants and types, and write a private helper function (ErrStr). This helper function links local ordinal constants to an error string resource that contains the actual error messages.

implementation
	uses
		Errors,
		ErrorDefinitions,
		ErrorHandler;

	const
		UnitName = 'MyUtilities';

	type
		oErrorString = (UnknownError,
			FileBufferErr1, { Couldn't open the file "filename". }
			FileBufferErr2	 { because ...}
			);

	function ErrStr (errorNumber: oErrorString): Str255;
	begin
	ErrStr := GetErrorString(ord(errorNumber), uMyUtilities);
	end;

Now we can implement a private helper function that allocates the two buffers.

	function AllocateHandles (
			var largeHandle, smallHandle: handle;
			size: integer): OSErr;

	const
		ProcName = 'AllocateHandles';

		SmallHandleError = 
		'Small handle allocation failed in AllocateHandles.';
		LargeHandleError = 
		'Large handle allocation failed in AllocateHandles.';

		SizeError = 
		'Trying to allocate handles with negative size.';

	var
		error: OSErr;

		procedure CleanupAndExit;
		begin
		AllocateHandles := memFullErr;

		if (largeHandle <> nil) then
			begin
			DisposeHandle(largeHandle);
			largeHandle := nil;
			end;
		if (smallHandle <> nil) then
			begin
			DisposeHandle(smallHandle);
			smallHandle := nil;
			end;

		Exit(AllocateHandles);
		end;

	begin

	if (size < 0) then
		ProgramError(SizeError, ProcName, UnitName);

	largeHandle := nil;
	smallHandle := nil;

	largeHandle := NewHandle(2 * size);
	if (largeHandle = nil) then
		LogError(LargeHandleError, CleanUpAndExit);

	{ Next line is commented out to simulate failure. } 
	{ smallHandle := NewHandle(size); }

	if (smallHandle = nil) then
		LogError(SmallHandleError, CleanUpAndExit);

	AllocateHandles := noErr;
	end;

This function has full error checking. The small handle allocation is commented out to simulate an allocation failure. This function is called from MyOpenFile, which in turn calls HandleError if it fails. It is appropriate for MyOpenFile to call HandleError because the file name should be part of the error message and AllocateHandles doesn't have access to it.

It is good practice to check each memory allocation immediately after trying to allocate. In the example above, a large allocation precedes a small allocation. It's entirely possible that the large allocation can fail but the small allocation succeeds. This is why you can't simply check the last allocation in a series of allocations. Also, if you use MemError to check an allocation, you have to check it immediately because its result is changed after each new allocation.

Notice how the exit statement in the CleanUpAndExit procedure gets us all the way out of AllocateHandles, not just the nested CleanUpAndExit procedure. Furthermore, this works when CleanUpAndExit is called from inside HandleError or LogError. This feature of Apple's Pascal lets us elegantly exit AllocateHandles from the nested procedure CleanUpAndExit so we don't have to clutter up the main code with explicit exit statements.

The AllocateHandles procedure also shows an example of using the ProgramError procedure. Notice how the arguments appear in order of increasing scope (message, procedure, unit). This helps you to remember the order. This is important because with the arguments all being the same type (Str255), you can mix up the order and the error will not be caught at compile time. On the other hand, if you forget to declare the UnitName or ProcName arguments, the compiler will catch it.

Finally, we write the public open file procedure. This procedure calls the private helper function, AllocateHandles, to allocate the two buffers. During the file open operation there are two classes of errors that might occur: file I/O errors and memory allocation errors. The user definitely needs to know which type of error has occurred, but they also need to know the file name. The exact details of why a memory allocation failed may not be useful to the end user, so we silently log the error, clean up, then handle the error at the level of the file open procedure where we have access to the file name. The MyOpenFile source is shown below.

function MyOpenFile (fileName: Str255;
		var fileData: tMYFile): OSErr;
	const
		kSmallBufferSize = 2000;

	var
		error: OSErr;

		function FileBufferErr: Str255;
			var
				errorString: Str255;
		begin
		errorString := ErrStr(FileBufferErr1);
		AppendQuote(errorString, fileName);
		SafeAppend(errorString, ErrStr(FileBufferErr2));
		FileBufferErr := errorString;
		end;

		procedure CleanupAndExit;
		begin
		MyOpenFile := error;
			{ Put clean up here. }
		Exit(MyOpenFile);
		end;

begin
error := AllocateHandles(fileData.largeBuffer,
		fileData.smallBuffer, kSmallBufferSize);
if (error <> noErr) then
	HandleError(FileBufferErr, CleanUpAndExit);

MyOpenFile := noErr;
end;

Aside: Apple Pascal "Exit" Procedure

The Object Pascal "Exit" procedure, available in Think Pascal and CodeWarrior Pascal, is an extension to Standard Pascal. However, its functionality can always be implemented using a goto statement from Standard Pascal, but the code is much less readable. In combination with nested procedures and functions, it is very useful for implementing error handling. This section describes its history and rationale.

Standard Pascal has only three iteration statements: the for statement, the while statement, and the repeat statement. The for statement is intended to be used only when you know exactly how many times you will iterate. The while and the repeat statements iterate a variable number of times but show their exit condition(s) at the start or the end of the enclosed iteration block. These are the natural locations to show exit conditions.

It's important for readability to be able to quickly determine the exit conditions of an iteration. If it's possible to have exit conditions in the interior of the iteration block, then the reader has to search through the block to understand how the iteration works. However, there are times when the most elegant thing to do is to exit part of the way through an iteration or exit from more than one nested block. So Standard Pascal has the goto statement to handle all these unusual situations that can't be handled gracefully using the three iteration statements. The goto lets you exit from the interior of a block or procedure to any outer block or procedure, so it works in conjunction with nested blocks, procedures, and functions.

In Apple's Pascal, the "Exit" statement takes a single argument, which is the name of a procedure or function from which to exit. This argument is only useful when you have nested procedures; you can exit immediately to the scope that you desire. For example, if procedure A contains procedure B and procedure B contains procedure C, you can exit directly from C to A. This is very useful for implementing an error handler module as we have seen.

This form of the exit statement dates back to UCSD Pascal, which was developed in the late 1970's. UCSD Pascal showed that efficient Pascal compilers could be implemented on microcomputers. It is one of the primary reasons Pascal became popular in the first place. Many of its extensions were carried on into Apple's Pascal and Borland's Turbo Pascal.

HandleError Implementation

procedure HandleError (errorMessage: Str255;
		procedure CleanUpAndExit);
begin
if (DisplayingError) then 
	begin
	DisplayingError := false;
	DisplayError(ConstructErrorText(errorMessage));
	end;

CleanUpAndExit;
end;

The display of the error dialog is protected by a public boolean variable: DisplayingError. DisplayingError is initialized to true and then is set to false only when the error message is displayed. The client can reset it by assigning it to true. This insures that only one error dialog is displayed until the client sets DisplayingError to true. The programmer can then freely use HandleError without having to know if it is called above or below the current procedure.

ConstructErrorText checks and prepares the message for the dialog box. It replaces the empty string with the "Unknown Error" string. It can be used to add titles and line breaks, if desired.

DisplayError shows the error dialog and waits until the user selects the OK button. It should work even when memory is low because it may be reporting a memory allocation failure! Its implementation will be discussed in a later section.

Memory Management Strategy

All three of HandleError, LogError, and ProgramError should work in low memory conditions. It's very important that the user knows what went wrong. It is not acceptable to "unexpectedly quit".

Whenever possible, our strategy will be to preallocate the memory we need. The string list resources that contain the error messages should be marked "preload" and "locked". They will then be automatically loaded into memory at startup. The ErrorHandler unit will be loaded into memory when you call InitErrorHandler. If your are developing for 68k, do not call Unloadseg on the error handler unit.

For the dialog, we will allocate a handle at startup large enough for the dialog and anything else needed to display the alert. When it comes time to show the alert, we will free the handle, show the alert, and then reallocate the handle. We want to use a handle so that we do not fragment memory when we do the reallocation.

Finally, we will store important state information in static variables so that we do not have to call procedures that may allocate memory to get this information after an error has occurred. This includes information about the log file and the reserve memory handle.

DisplayError Implementation

This procedure needs to display a standard "stop" alert with the error message. This message may be from 1 to 255 characters in length. A dialog large enough to hold a 255 character string will look unprofessional with only a few words in it. Our strategies range from always displaying the same large dialog to dynamically sizing the dialog for each message. Dynamic sizing is complicated by the possibility that the message may be in other languages, some of which are so large that they require two bytes per character (Japanese, Chinese) and some of which are read from right-to-left (Arabic, Hebrew).

The approach taken here is to determine how many lines we need and adjust the height of a default dialog which is stored as a resource. The first step is to count the number of lines required to fit the message within the width of our default dialog text field after proper line breaking. Multiplying the number of lines times the line height gives us the height of the text field. If it is smaller than the height of our default text field, then we simply display the error. If it is less than some reasonable maximum height, then we adjust the height of the dialog accordingly. If it is larger than the maximum height, then we let the string run off the end of our largest allowed text field. Don't worry, the dialog manager will clip the text to the available text field area.

The utility function CountLines calls the Toolbox routine StyledLineBreak to compute the number of lines that the dialog manager will use to display the message in the system font.

The source for DisplayError is shown below.

procedure DisplayError(errorMessage: Str255);
begin
if (sReservedSpace <> nil) then
	begin
	disposeHandle(sReservedSpace);
	sReservedSpace := nil;

	ErrorAlert(errorMessage);

	ReserveMem(DisplayBytes);
	sReservedSpace := NewHandle(DisplayBytes);
	if (sReservedSpace = nil) then
		Halt;
	end
else
	Halt;
end;

DisplayError uses one static variable: sReservedSpace. The identifier is prefixed by a small "s" for "static". sReservedSpace is initialized in InitErrorHandler (to be discussed later).

If our reserve memory is not available, then something is seriously wrong with out memory management. The error handler has probably already displayed an error, so we halt.

ErrorAlert Implementation

ErrorAlert is implemented using ModalDialog as follows:

procedure ErrorAlert (errorMessage: Str255);
	var
		savePort: GrafPtr;
		dialogFontInfo: FontInfo;
		mainScreen: GDHandle;
		lines: integer;
		lineHeight: integer;

		heightChange: integer;
		textHeightPixels: integer;
		textWidthPixels: integer;
		windowWidth: integer;
		windowHeight: integer;
		newTextHeight: integer;

		theDialog: DialogPtr;
		itemHandle: Handle;
		itemType: integer;

		textHandle: Handle;
		textRect: Rect;
		buttonHandle: ControlHandle;
		buttonRect: Rect;
		windowHGlobal: integer;
		windowVGlobal: integer;

		itemHit: integer;
begin
	{ Deactivate your top window here. }

theDialog := GetNewDialog(kErrorAlertID, nil, Pointer(-1));

	{ Make sure the dialog's GrafPort is set to the System font and style. }

GetPort(savePort);
SetPort(theDialog);

TextFont(GetSysFont);
TextSize(12);
TextFace([]);
SpaceExtra(0);

	{ Get the line height (in pixels) of the dialog's font. }

GetFontInfo(dialogFontInfo);
with dialogFontInfo do
	lineHeight := ascent + descent + leading;

	{ Get the size of the dialog. }

with theDialog^.portRect do
	begin
	windowWidth := right - left;
	windowHeight := bottom - top;
	end;

	{ Get the size of the text field. }

GetDItem(theDialog, kErrorTextItem, itemType, textHandle,
		textRect);
with textRect do
	begin
	textHeightPixels := bottom - top;
	textWidthPixels := right - left;
	end;

lines := CountLines(errorMessage, textWidthPixels,
		 GrafPtr(theDialog));

newTextHeight := lines * lineHeight;
if (newTextHeight > kTextHeightMax) then
	newTextHeight := kTextHeightMax;

heightChange := newTextHeight - textHeightPixels;

if (heightChange > 0) then
	begin
		{ Increase the size of the dialog. }
	windowHeight := windowHeight + heightChange;
	SizeWindow(theDialog, windowWidth, windowHeight, true);

		{ Move the OK button down. }

	GetDItem(theDialog, kErrorOKItem, itemType, itemHandle,
			buttonRect);
	buttonHandle := ControlHandle(itemHandle);
	OffsetRect(buttonRect, 0, heightChange);
	with buttonRect do
		MoveControl(buttonHandle, left, top);
	SetDItem(theDialog, kErrorOKItem, itemType, itemHandle,
			buttonRect);

		{ Extend the bottom of the text field. }

	textRect.bottom := textRect.bottom + heightChange;
	SetDItem(theDialog, kErrorTextItem, statText, textHandle,
			textRect);
	end;

SetDialogItemText(textHandle, errorMessage);

	{ Center the dialog on the main screen. }

mainScreen := GetMainDevice;
with mainScreen^^.gdRect do
	begin
	windowHGlobal := (left + right - windowWidth) div 2;
	windowVGlobal := (top + bottom - windowHeight) div 2;
	end;
MoveWindow(theDialog, windowHGlobal, windowVGlobal, true);

ShowWindow(theDialog);

SysBeep(1);
SetCursor(qd.arrow);
repeat
	ModalDialog(nil, itemHit);
until (itemHit = kErrorOKItem);

SetPort(savePort);

DisposeWindow(theDialog);
end;

This code basically creates the specified dialog, adjusts the size of the dialog to contain the error message, replaces the static text with the error message, beeps, shows and handles the dialog, then destroys the dialog. The dialog contains just three items: the OK button, the stop icon, and the static text field and they should be numbered in that order. Note that the static text field should be enabled.

According to Inside Macintosh: Macintosh Toolbox Essentials (P. 6-64) you will need to deactivate your top window using whatever window management scheme you have implemented. This is because modal dialog traps all events once you call it, including deactivate events.

CountLines Implementation

If you want to do your own line breaks, or, as here, simply count line breaks, you will need to learn about the Toolbox routine StyledLineBreak. This magical routine will correctly break lines in 27 different writing systems (Guide to Macintosh Software Localization). All of these writing systems can be read from left-to-right or right-to-left except for one: Mongolian. For just counting lines, we don't care whether it's left-to-right or right-to-left. However, Mongolian must be read from top-to-bottom, then left-to-right. This means CountLines will not work properly for Mongolian (26 out of 27 isn't bad). Here is the source.

function CountLines (theText: Str255;
		fieldWidthPixels: integer;
		theGrafPort: GrafPtr): integer;
	var
		lineCount: integer;
		lineStart: LongInt;
		textPtr: Ptr;
		lineBytes: LongInt;
		widthPixels: Fixed;
		linePixels: Fixed;
		breakBytes: LongInt;
		breakCode: StyledLineBreakCode;

		savePort: GrafPtr;
begin
if (Length(theText) = 0) then
	begin
	CountLines := 1;
	Exit(CountLines);
	end;

GetPort(savePort);
SetPort(theGrafPort);

widthPixels := Long2Fix(LongInt(fieldWidthPixels)); { FixMath.p }
lineCount := 0;
lineStart := 1;
lineBytes := Length(theText);

repeat
	lineCount := lineCount + 1;
	linePixels := widthPixels;
	breakBytes := 1;
	textPtr := @theText[lineStart];

	breakCode := StyledLineBreak(textPtr, lineBytes, 0, 
			lineBytes, 0, linePixels, breakBytes);

	lineStart := lineStart + breakBytes;
	lineBytes := lineBytes - breakBytes;
until (breakCode = smBreakOverflow);

SetPort(savePort);

CountLines := lineCount;
end;

CountLines computes the number of lines that will be required by the dialog manager to fit in a text field of a specified width in pixels using the system font. The hard work is done by StyledLineBreak. Since the dialog manager uses StyledLineBreak, you should get exactly the number of lines that CountLines reports when you actually show the dialog. Note that you need to include FixMath.p in your project to convert the integer field width to the Fixed data type.

Using StyledLineBreak means that when it comes time to localize your error messages, all you have to do is edit the string resources (assuming you know the other language), and not fool around with line breaks in custom dialog boxes.

For a more general treatment of fitting text into dialog boxes see Bryan Ressler's excellent article "The TextBox You've Always Wanted" (Ressler, 1992).

LogError Implementation

LogError's job is to write the specified error message to an error log file in the directory where your application is. The volume and folder is determined and saved when the ErrorHandler unit is initialized (InitErrorHandler). If the file doesn't exist when it comes time to write an error message, LogError creates it.

This version creates a read-only SimpleText file. The sophisticated user or you can simply double-click it to read the errors. Since the file is read-only, the modification date gives the time the last error was written. You could write other information at startup like the date, the system version, etc. You could also write the date and time before each error message, but I have chosen to keep it simple here.

Even though this is inefficient, we open and close the file for each error message. We even flush the volume to make sure that the changed directory data structure is written to disk right after writing the message. The reason is that this might turn out to be the last chance to report an error before the application crashes. Okay, you can call me paranoid. Here's the code.

procedure LogError (errorMessage: Str255;
		procedure CleanUpAndExit);
	const
		kReadOnly = 'ttro'; { read only Simple Text file }
		kSimpleText = 'ttxt';

	var
		error: OSErr;
		logFileSpec: FSSpec;
		refNum: integer;
		dividend: integer;
		digits: integer;
		theText: Str255;
		numBytes: Longint;

begin
if (sLogErrorCount < kMaxLogErrors) then
	begin
	sLogErrorCount := sLogErrorCount + 1;
	refNum := 0;

	error := FSMakeFSSpec(sAppVRefNum, sAppDirID, 
			sLogFileName, logFileSpec);

	if (error = fnfErr) then	{ File doesn't exist; }
													{ create an empty one. }
		error := FSpCreate(logFileSpec, 
				kSimpleText, kReadOnly, smSystemScript);

	if (error = noErr) then { The file exists; open it. }
		error := FSpOpenDF(logFileSpec, fsRdWrPerm, refNum);

	if (error = noErr) then
		if (sLogErrorCount = 1) then { Overwrite the old file. }
			error := SetEOF(refNum, 0);

	if (error = noErr) then
		error := SetFPos(refNum, fsFromLEOF, 0);

	if (error = noErr) then
		begin
		digits := 0;
		dividend := sLogErrorCount;
		while (dividend > 0) do
			begin
			dividend := dividend div 10;
			digits := digits + 1;
			end;

		theText := Concat(StringOf(
				sLogErrorCount : digits), '. ');

		SafeAppend(theText, errorMessage);
		SafeAppend(theText, returnChar);
		SafeAppend(theText, returnChar);

		numBytes := Length(theText);
		error := FSWrite(refNum, numBytes, @theText[1]);
		end;

	if (refNum > 0) then
		begin
		error := FSClose(refNum);
		refNum := 0;
		error := FlushVol(nil, logFileSpec.vRefNum);
		end;
	end;

CleanUpAndExit;
end;

Note that we don't attempt to call HandleError if any of the file operations fail. It would be inappropriate to notify the user about the failure of an operation that they don't know about and didn't request.

ProgramError Implementation

ProgramError constructs a message to tell the developer what the error is and where it occurred in the code. This is similar to an assertion, but it is part of the shipping code.

procedure ProgramError (errorMessage, procName, 
		unitName: Str255);
begin
DisplayError(LastWords(errorMessage, procName, unitName));
Halt;
end;

The procName and unitName arguments are typically local string constants. LastWords basically adds titles and line breaks for the procName and unitName strings.

function LastWords (errorMessage, 
		procName, unitName: Str255):Str255;
	var
		suffix: Str255;
		temporaryString: Str255;
		excessCharacters: integer;
		prefixLength: integer;
		theLastWords: Str255;
begin
if (errorMessage = '') then
	errorMessage := ErrStr(kUnknownError);
if (procName = '') then
	procName := ErrStr(kUnknown);
if (unitName = '') then
	unitName := ErrStr(kUnknown);

theLastWords := ErrStr(kFatalTitle);

prefixLength := Length(theLastWords);

suffix := Concat(returnChar, returnChar, ErrStr(kProcTitle));
SafeAppend(suffix, procName);
temporaryString := Concat(returnChar, returnChar,
		ErrStr(kUnitTitle));
SafeAppend(suffix, temporaryString);
SafeAppend(suffix, unitName);

excessCharacters := prefixLength + Length(suffix) - 255;
if (excessCharacters > 0) then
	TrimStringTail(errorMessage, excessCharacters);

SafeAppend(theLastWords, errorMessage);
SafeAppend(theLastWords, suffix);

LastWords := theLastWords;
end;

The SafeAppend and TrimStringTail string utilities are part of a string utilities unit. They will not be described but are available on the Mac Tech ftp site at <ftp://ftp.mactech.com>.

String Utilities

Much of the code in error handling is just string manipulation. We need to get the correct string from a resource, possibly append a quoted string that the end user understands, and put strings together without overrunning allocated memory. For these three things, I provide GetErrorString, AppendQuote, and SafeAppend.

GetErrorString makes it easy for you, the client, to map resource strings to private ordinal constants. Ordinal constants are safer than integer constants because range errors are caught at compile time. The problem is that these ordinal constants should be hidden in the implementation section of the unit where they are used. This prevents outside access and avoids name conflicts, but it also hides them from the ErrorHandler unit.

The idiom for connecting these private ordinal constants to the actual resource strings is as follows. Create a unit called ErrorDefinitions that declares an ordinal type that maps ordinal constants to a series of string list resources. Prefix each constant with a lower case "u" (short for unit), for example, "uMyUtilities". Provide a function GetErrorStringResourceID that maps each ordinal constant to its resource ID. The most direct way to do this is to use a case statement.

unit ErrorDefinitions;

interface
	const
		ProgramName = 'MyApplication';

	type
		oUnitID = (
			uBeforeFirst,
			uMyUtilities,
			uAfterLast);

	function GetErrorStringResourceID (
			unitID: oUnitID): integer;

implementation

	function GetErrorStringResourceID (
			unitID: oUnitID): integer;
	begin
	case unitID of
	uMyUtilities: 
		GetErrorStringResourceID := 400;
	otherwise
		GetErrorStringResourceID := 0;
	end;
	end;
end.

Next create a private function that maps your private ordinal type to a string in the string list resource corresponding to this unit. This private function uses GetErrorString to do the bookkeeping. Typically, the name of this function is ErrStr to keep it short so that the HandleError call can be done on one line. For the MyApplication example, see the code at the top of the implementation in the section "Using the Error Handler: MyApplication Example".

We use the Pascal built in function "ord" to convert the ordinal constant to an integer for GetErrorString. The ordinal type should have the same number and order as the error strings in your string resource list except for the first element, which is given the name "UnknownError". The ord of the first element of an ordinal type is "0" but the first string in a string resource list is number "1". Ordinarily, the UnknownError constant will not be used.

The ordinal constant identifiers, such as "FileBufferErr1", should be fairly verbose, since they substitute for the error message in your code. On the other hand, they shouldn't be so long that we need to use two lines of code to call HandleError.

The ErrorHandler function GetErrorString gets the specified error string from the MyUtilities resource string list using the toolbox routine GetIndString. If you have forgotten to add the unit identifier uMyUtilities to ErrorDefinitions.p, this will be caught at compile time when you try to compile your local ErrStr. If you have forgotten to create the resource string list, this will be caught at startup by the procedure CheckErrorStrings, which tries to open all of the resource error, string lists you have specified in ErrorDefinitions. The code for GetErrorString appears below.

function GetErrorString (errorNumber: integer; 
		unitID: oUnitID): Str255;
	var
		theErrorMessage: Str255;
		stringResourceID: integer;
begin
if (errorNumber = 0) then
	GetIndString(theErrorMessage, kErrorStringsID,
			ord(kUnknownError) + 1)
else
	begin
	stringResourceID := GetErrorStringResourceID(unitID);
	if (stringResourceID > 0) then
		GetIndString(theErrorMessage, stringResourceID,
				errorNumber)
	else
		GetIndString(theErrorMessage, kErrorStringsID,
				ord(MissingErrorStringListErr) + 1);
	end;

GetErrorString := theErrorMessage;
end;

If you forget to add the error string to the resource string list, GetIndString will return the empty string and, unfortunately, this will occur at runtime. If you use GetErrorString to pass the string to LogError, it will simply show the empty string or the unknown error string. To help avoid this type of error, the procedure TestUnitErrors is provided. TestUnitErrors displays each message in a specified unit.

Sometimes the error message cannot be stored in advance and must be constructed on the fly. For example, in MyOpenFile, we want to include the file name as part of the message. In this case we can create a nested function that returns the constructed error message (see the function FileBufferErr in the section "Using the Error Handler: MyApplication Example").

In this example, FileBufferErr constructs the following message:

MyApplication could not open the file "MyFile" because there is not enough memory to allocate the required file buffers.

Try closing MyApplication windows, quitting other applications, or giving MyApplication more memory using the Get Info dialog.

"MyFile" is the file name used by our test program. FileBufferErr1 contains the message before the quoted file name. FileBufferErr2 contains the rest of the message.

AppendQuote is a helper function in the ErrorHandler unit to put the proper curly double quotes around a string that you want to append to another string. SafeAppend concatenates two strings using the first string's storage. If the second string is too long to fit in the first string's remaining storage (maximum 255 bytes), then the second string is truncated to fit. AppendQuote uses SafeAppend as follows:

procedure AppendQuote (var message: Str255; 
		theQuote: Str255);
	const
		LeftQuotes = chr(210);
		RightQuotes = chr(211);
begin
SafeAppend(message, LeftQuotes);
SafeAppend(message, theQuote);
SafeAppend(message, RightQuotes);
end;

Odds and Ends

This section cleans up this article by describing the ErrorHandler unit private constants, types, and variables. It also documents the InitErrorHandler function to be called at startup of the program.

The constant section is shown below.

The first constant, DisplayBytes, is the number of bytes reserved for displaying the error dialog. This includes any additional heap space required by the system to display the dialog.

The second constant is the maximum number of errors in the error log. It should be less than about 128 so that even if the strings are full (255 bytes each), the total space cannot exceed 32,767 which is (still) the limit for SimpleText.

The next five items refer to the error dialog. The first two constants are the resource ID's of the string list used by the error handler and the error dialog, respectively. The next two constants are the dialog item numbers of the OK button and the text field where the error message will be displayed. The next item is the maximum allowed height of the text field in pixels. The width is always the same but the height varies.

The last three constants are self-explanatory.

const
	kDisplayBytes = 5 * 1024; { heap space for error dialog }
	kMaxLogErrors = 100;

	kErrorStringsID = 200;	{ resource ID of ErrorHandler strings }

	kErrorAlertID = 401;
	kErrorOKItem = 1;
	kErrorTextItem = 3;
	kTextHeightMax = 200;
	UnitName = 'ErrorHandler';

	returnChar = chr(13);
	tabChar = chr(9);

The Error Handler uses ordinal types exactly like the user's units. It uses the oErrorString ordinal type defined as follows:

type
	oErrorString = (
		kUnknownError,	{ "Unknown error." }
		kUnknown,			{ "Unknown." }
		kFatalTitle,		{ "Programming Error: " }
		kProcTitle,		{ "Where: " }
		kUnitTitle,		{ "Unit: " }
		MissingErrorStringListErr	
			{ "An error string list resource is missing." }
		);

The messages associated with these constants are found in the "Error Handler Strings" resource of type "STR#" in the file "ErrorHandler.rsrc". The local ErrStr function is slightly different than the client's local ErrStr function. The difference between the ordinal constant offset (0) and the string list offset (1) is hidden for the client by GetErrorString. This explains the "+ 1" in the ErrorHandler module's ErrStr.

function ErrStr (errorNumber: oErrorString): Str255;
	var
		theErrorMessage: Str255;
begin
GetIndString(theErrorMessage, kErrorStringsID, 
		ord(errorNumber) + 1);
ErrStr := theErrorMessage;
end;

The var (or variable) section of the implementation contains static variables (prefix "s") that are allocated at startup in the global storage area. This means they will likely be available when an error message needs to be displayed. Most of these variables have already been discussed.

var
	sLogErrorCount: integer;
	sLogFileName: Str255;
	sReservedSpace: Handle;
	sAppVRefNum: integer;
	sAppDirID: Longint;

The InitErrorHandler function allocates the reserve memory required to display an error as well as obtaining information about the application's volume and folder.

function InitErrorHandler: boolean;
	var
		error: OSErr;
begin
DisplayingError := true;
sLogErrorCount := 0;

sLogFileName := ProgramName;
SafeAppend(sLogFileName, '.log');

error := HGetVol(nil, sAppVRefNum, sAppDirID);

sReservedSpace := nil;
ReserveMem(DisplayBytes);
sReservedSpace := NewHandleClear(DisplayBytes);

InitErrorHandler := (error = noErr) and 
		(sReservedSpace <> nil);
end;

DisplayingError gives the client control over when the error handler is reset to fire again. It is a flag that is intended to prevent multiple messages for the same error. You typically assign it to true in your main event loop.

The following procedures and functions have not been described explicitly but are available at the Mac Tech web site: SafeAppend, TrimStringTail, FreeErrorHandler, ConstructErrorText, CheckErrorStrings, TestUnitErrors, and TestAllErrors.

Conclusion

I have presented a simple error handler module in Apple's version of Pascal. It provides general utility procedures for displaying error messages, executing client defined clean up procedures, and exiting the failed procedure. The three workhorse procedures: HandleError, LogError, and ProgramError, give you the flexibility to handle errors ranging from nonfatal errors, such as memory exhaustion, to fatal errors caused by software bugs. Care has been taken to make these error handling procedures work even when the application is out of memory. In addition, there are string-handling utilities that let you prepare messages for dialog boxes and extract error messages from string resources so you can easily localize your application. This error handler module, or something like it, is essential to make your application professional.

Bibliography

  • Apple Computer. Guide to Macintosh Software Localization, Addison-Wesley Publishing Company, 1992.
  • Apple Computer. Inside Macintosh: Macintosh Toolbox Essentials, Addison-Wesley Publishing Company, 1992.
  • Lewis, Peter N. "Using Assert()", MacTech, December 97.
  • Maguire, Steve. Writing Solid Code, Microsoft Press, 1993.
  • Parsons, Paige K. "Guidelines for Effective Alerts", develop, Issue 24, December 1995.
  • Ressler, Bryan K. "The Textbox You've Always Wanted", develop, Issue 9, Winter 92.

Jim Phillips has been programming in Pascal on the Macintosh since 1986. He is an aeronautical engineer by training, but he would rather write programs to do engineering than do engineering. Send comments to jdp@got.net.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Latest Forum Discussions

See All

Tokkun Studio unveils alpha trailer for...
We are back on the MMORPG news train, and this time it comes from the sort of international developers Tokkun Studio. They are based in France and Japan, so it counts. Anyway, semantics aside, they have released an alpha trailer for the upcoming... | Read more »
Win a host of exclusive in-game Honor of...
To celebrate its latest Jujutsu Kaisen crossover event, Honor of Kings is offering a bounty of login and achievement rewards kicking off the holiday season early. [Read more] | Read more »
Miraibo GO comes out swinging hard as it...
Having just launched what feels like yesterday, Dreamcube Studio is wasting no time adding events to their open-world survival Miraibo GO. Abyssal Souls arrives relatively in time for the spooky season and brings with it horrifying new partners to... | Read more »
Ditch the heavy binders and high price t...
As fun as the real-world equivalent and the very old Game Boy version are, the Pokemon Trading Card games have historically been received poorly on mobile. It is a very strange and confusing trend, but one that The Pokemon Company is determined to... | Read more »
Peace amongst mobile gamers is now shatt...
Some of the crazy folk tales from gaming have undoubtedly come from the EVE universe. Stories of spying, betrayal, and epic battles have entered history, and now the franchise expands as CCP Games launches EVE Galaxy Conquest, a free-to-play 4x... | Read more »
Lord of Nazarick, the turn-based RPG bas...
Crunchyroll and A PLUS JAPAN have just confirmed that Lord of Nazarick, their turn-based RPG based on the popular OVERLORD anime, is now available for iOS and Android. Starting today at 2PM CET, fans can download the game from Google Play and the... | Read more »
Digital Extremes' recent Devstream...
If you are anything like me you are impatiently waiting for Warframe: 1999 whilst simultaneously cursing the fact Excalibur Prime is permanently Vault locked. To keep us fed during our wait, Digital Extremes hosted a Double Devstream to dish out a... | Read more »
The Frozen Canvas adds a splash of colou...
It is time to grab your gloves and layer up, as Torchlight: Infinite is diving into the frozen tundra in its sixth season. The Frozen Canvas is a colourful new update that brings a stylish flair to the Netherrealm and puts creativity in the... | Read more »
Back When AOL WAS the Internet – The Tou...
In Episode 606 of The TouchArcade Show we kick things off talking about my plans for this weekend, which has resulted in this week’s show being a bit shorter than normal. We also go over some more updates on our Patreon situation, which has been... | Read more »
Creative Assembly's latest mobile p...
The Total War series has been slowly trickling onto mobile, which is a fantastic thing because most, if not all, of them are incredibly great fun. Creative Assembly's latest to get the Feral Interactive treatment into portable form is Total War:... | Read more »

Price Scanner via MacPrices.net

Early Black Friday Deal: Apple’s newly upgrad...
Amazon has Apple 13″ MacBook Airs with M2 CPUs and 16GB of RAM on early Black Friday sale for $200 off MSRP, only $799. Their prices are the lowest currently available for these newly upgraded 13″ M2... Read more
13-inch 8GB M2 MacBook Airs for $749, $250 of...
Best Buy has Apple 13″ MacBook Airs with M2 CPUs and 8GB of RAM in stock and on sale on their online store for $250 off MSRP. Prices start at $749. Their prices are the lowest currently available for... Read more
Amazon is offering an early Black Friday $100...
Amazon is offering early Black Friday discounts on Apple’s new 2024 WiFi iPad minis ranging up to $100 off MSRP, each with free shipping. These are the lowest prices available for new minis anywhere... Read more
Price Drop! Clearance 14-inch M3 MacBook Pros...
Best Buy is offering a $500 discount on clearance 14″ M3 MacBook Pros on their online store this week with prices available starting at only $1099. Prices valid for online orders only, in-store... Read more
Apple AirPods Pro with USB-C on early Black F...
A couple of Apple retailers are offering $70 (28%) discounts on Apple’s AirPods Pro with USB-C (and hearing aid capabilities) this weekend. These are early AirPods Black Friday discounts if you’re... Read more
Price drop! 13-inch M3 MacBook Airs now avail...
With yesterday’s across-the-board MacBook Air upgrade to 16GB of RAM standard, Apple has dropped prices on clearance 13″ 8GB M3 MacBook Airs, Certified Refurbished, to a new low starting at only $829... Read more
Price drop! Apple 15-inch M3 MacBook Airs now...
With yesterday’s release of 15-inch M3 MacBook Airs with 16GB of RAM standard, Apple has dropped prices on clearance Certified Refurbished 15″ 8GB M3 MacBook Airs to a new low starting at only $999.... Read more
Apple has clearance 15-inch M2 MacBook Airs a...
Apple has clearance, Certified Refurbished, 15″ M2 MacBook Airs now available starting at $929 and ranging up to $410 off original MSRP. These are the cheapest 15″ MacBook Airs for sale today at... Read more
Apple drops prices on 13-inch M2 MacBook Airs...
Apple has dropped prices on 13″ M2 MacBook Airs to a new low of only $749 in their Certified Refurbished store. These are the cheapest M2-powered MacBooks for sale at Apple. Apple’s one-year warranty... Read more
Clearance 13-inch M1 MacBook Airs available a...
Apple has clearance 13″ M1 MacBook Airs, Certified Refurbished, now available for $679 for 8-Core CPU/7-Core GPU/256GB models. Apple’s one-year warranty is included, shipping is free, and each... Read more

Jobs Board

Seasonal Cashier - *Apple* Blossom Mall - J...
Seasonal Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Seasonal Fine Jewelry Commission Associate -...
…Fine Jewelry Commission Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) Read more
Seasonal Operations Associate - *Apple* Blo...
Seasonal Operations Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Read more
Hair Stylist - *Apple* Blossom Mall - JCPen...
Hair Stylist - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Read more
Cashier - *Apple* Blossom Mall - JCPenney (...
Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Mall Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.