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

Amikin Survival opens for pre-orders on...
Join me on the wonderful trip down the inspiration rabbit hole; much as Palworld seemingly “borrowed” many aspects from the hit Pokemon franchise, it is time for the heavily armed animal survival to also spawn some illegitimate children as Helio... | Read more »
PUBG Mobile teams up with global phenome...
Since launching in 2019, SpyxFamily has exploded to damn near catastrophic popularity, so it was only a matter of time before a mobile game snapped up a collaboration. Enter PUBG Mobile. Until May 12th, players will be able to collect a host of... | Read more »
Embark into the frozen tundra of certain...
Chucklefish, developers of hit action-adventure sandbox game Starbound and owner of one of the cutest logos in gaming, has released their roguelike deck-builder Wildfrost. Created alongside developers Gaziter and Deadpan Games, Wildfrost will... | Read more »
MoreFun Studios has announced Season 4,...
Tension has escalated in the ever-volatile world of Arena Breakout, as your old pal Randall Fisher and bosses Fred and Perrero continue to lob insults and explosives at each other, bringing us to a new phase of warfare. Season 4, Into The Fog of... | Read more »
Top Mobile Game Discounts
Every day, we pick out a curated list of the best mobile discounts on the App Store and post them here. This list won't be comprehensive, but it every game on it is recommended. Feel free to check out the coverage we did on them in the links below... | Read more »
Marvel Future Fight celebrates nine year...
Announced alongside an advertising image I can only assume was aimed squarely at myself with the prominent Deadpool and Odin featured on it, Netmarble has revealed their celebrations for the 9th anniversary of Marvel Future Fight. The Countdown... | Read more »
HoYoFair 2024 prepares to showcase over...
To say Genshin Impact took the world by storm when it was released would be an understatement. However, I think the most surprising part of the launch was just how much further it went than gaming. There have been concerts, art shows, massive... | Read more »
Explore some of BBCs' most iconic s...
Despite your personal opinion on the BBC at a managerial level, it is undeniable that it has overseen some fantastic British shows in the past, and now thanks to a partnership with Roblox, players will be able to interact with some of these... | Read more »
Play Together teams up with Sanrio to br...
I was quite surprised to learn that the massive social network game Play Together had never collaborated with the globally popular Sanrio IP, it seems like the perfect team. Well, this glaring omission has now been rectified, as that instantly... | Read more »
Dark and Darker Mobile gets a new teaser...
Bluehole Studio and KRAFTON have released a new teaser trailer for their upcoming loot extravaganza Dark and Darker Mobile. Alongside this look into the underside of treasure hunting, we have received a few pieces of information about gameplay... | Read more »

Price Scanner via MacPrices.net

Amazon is offering a $200 discount on 14-inch...
Amazon has 14-inch M3 MacBook Pros in stock and on sale for $200 off MSRP. Shipping is free. Note that Amazon’s stock tends to come and go: – 14″ M3 MacBook Pro (8GB RAM/512GB SSD): $1399.99, $200... Read more
Sunday Sale: 13-inch M3 MacBook Air for $999,...
Several Apple retailers have the new 13″ MacBook Air with an M3 CPU in stock and on sale today for only $999 in Midnight. These are the lowest prices currently available for new 13″ M3 MacBook Airs... Read more
Multiple Apple retailers are offering 13-inch...
Several Apple retailers have 13″ MacBook Airs with M2 CPUs in stock and on sale this weekend starting at only $849 in Space Gray, Silver, Starlight, and Midnight colors. These are the lowest prices... Read more
Roundup of Verizon’s April Apple iPhone Promo...
Verizon is offering a number of iPhone deals for the month of April. Switch, and open a new of service, and you can qualify for a free iPhone 15 or heavy monthly discounts on other models: – 128GB... Read more
B&H has 16-inch MacBook Pros on sale for...
Apple 16″ MacBook Pros with M3 Pro and M3 Max CPUs are in stock and on sale today for $200-$300 off MSRP at B&H Photo. Their prices are among the lowest currently available for these models. B... Read more
Updated Mac Desktop Price Trackers
Our Apple award-winning Mac desktop price trackers are the best place to look for the lowest prices and latest sales on all the latest computers. Scan our price trackers for the latest information on... Read more
9th-generation iPads on sale for $80 off MSRP...
Best Buy has Apple’s 9th generation 10.2″ WiFi iPads on sale for $80 off MSRP on their online store for a limited time. Prices start at only $249. Sale prices for online orders only, in-store prices... Read more
15-inch M3 MacBook Airs on sale for $100 off...
Best Buy has Apple 15″ MacBook Airs with M3 CPUs on sale for $100 off MSRP on their online store. Prices valid for online orders only, in-store prices may vary. Order online and choose free shipping... Read more
24-inch M3 iMacs now on sale for $150 off MSR...
Amazon is now offering a $150 discount on Apple’s new M3-powered 24″ iMacs. Prices start at $1149 for models with 8GB of RAM and 256GB of storage: – 24″ M3 iMac/8-core GPU/8GB/256GB: $1149.99, $150... Read more
15-inch M3 MacBook Airs now on sale for $150...
Amazon is now offering a $150 discount on Apple’s new M3-powered 15″ MacBook Airs. Prices start at $1149 for models with 8GB of RAM and 256GB of storage: – 15″ M3 MacBook Air/8GB/256GB: $1149.99, $... Read more

Jobs Board

Early Preschool Teacher - Glenda Drive/ *Appl...
Early Preschool Teacher - Glenda Drive/ Apple ValleyTeacher Share by Email Share on LinkedIn Share on Twitter Read more
Retail Assistant Manager- *Apple* Blossom Ma...
Retail Assistant Manager- APPLE BLOSSOM MALL Brand: Bath & Body Works Location: Winchester, VA, US Location Type: On-site Job ID: 04225 Job Area: Store: Management Read more
Housekeeper, *Apple* Valley Village - Cassi...
Apple Valley Village Health Care Center, a senior care campus, is hiring a Part-Time Housekeeper to join our team! We will train you for this position! In this role, Read more
Sonographer - *Apple* Hill Imaging Center -...
Sonographer - Apple Hill Imaging Center - Evenings Location: York Hospital, York, PA Schedule: Full Time Sign-On Bonus Eligible Remote/Hybrid Regular Apply Now See Read more
Senior Software Engineer - *Apple* Fundamen...
…center of Microsoft's efforts to empower our users to do more. The Apple Fundamentals team focused on defining and improving the end-to-end developer experience in Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.