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.

 
AAPL
$102.13
Apple Inc.
+1.24
MSFT
$44.87
Microsoft Corpora
-0.14
GOOG
$571.00
Google Inc.
-6.86

MacTech Search:
Community Search:

Software Updates via MacUpdate

Viber 4.2.2 - Send messages and make cal...
Viber lets you send free messages and make free calls to other Viber users, on any device and network, in any country! Viber syncs your contacts, messages and call history with your mobile device,... Read more
Cocktail 7.6 - General maintenance and o...
Cocktail is a general purpose utility for OS X that lets you clean, repair and optimize your Mac. It is a powerful digital toolset that helps hundreds of thousands of Mac users around the world get... Read more
LaunchBar 6.1 - Powerful file/URL/email...
LaunchBar is an award-winning productivity utility that offers an amazingly intuitive and efficient way to search and access any kind of information stored on your computer or on the Web. It provides... Read more
Maya 2015 - Professional 3D modeling and...
Maya is an award-winning software and powerful, integrated 3D modeling, animation, visual effects, and rendering solution. Because Maya is based on an open architecture, all your work can be scripted... Read more
BBEdit 10.5.12 - Powerful text and HTML...
BBEdit is the leading professional HTML and text editor for the Mac. Specifically crafted in response to the needs of Web authors and software developers, this award-winning product provides a... Read more
Microsoft Office 2011 14.4.4 - Popular p...
Microsoft Office 2011 helps you create professional documents and presentations. And since Office for Mac 2011 is compatible with Office for Windows, you can work on documents with virtually anyone... Read more
TextWrangler 4.5.10 - Free general purpo...
TextWrangler is the powerful general purpose text editor, and Unix and server administrator's tool. Oh, and also, like the best things in life, it's free. TextWrangler is the "little brother" to... Read more
BitTorrent Sync 1.4.72 - Sync files secu...
BitTorrent Sync allows you to sync unlimited files between your own devices, or share a folder with friends and family to automatically sync anything. File transfers are encrypted. Your information... Read more
Cyberduck 4.5.2 - FTP and SFTP browser....
Cyberduck is a robust FTP/FTP-TLS/SFTP browser for the Mac whose lack of visual clutter and cleverly intuitive features make it easy to use. Support for external editors and system technologies such... Read more
Tinderbox 6.0.3 - Store and organize you...
Tinderbox is a personal content management assistant. It stores your notes, ideas, and plans. It can help you organize and understand them. And Tinderbox helps you share ideas through Web journals... Read more

Latest Forum Discussions

See All

Mobile Convolution (Music)
Mobile Convolution 1.0.0 Device: iOS Universal Category: Music Price: $9.99, Version: 1.0.0 (iTunes) Description: | Read more »
Invaders! From Outer Space Review
Invaders! From Outer Space Review By Rob Thomas on August 27th, 2014 Our Rating: :: RETRO NOSTALGIAUniversal App - Designed for iPhone and iPad It’s a shame that Invaders! doesn’t offer deeper gameplay, as this retro-inspired... | Read more »
Spooklands Review
Spooklands Review By Jennifer Allen on August 27th, 2014 Our Rating: :: ONE-TOUCH SHOOTERUniversal App - Designed for iPhone and iPad One-touch simultaneously controls your direction and your weapon in this unique arena shooter.   | Read more »
Heroes of Order & Chaos Add Twitch I...
Heroes of Order & Chaos Add Twitch Integration, New Heroes, and More Posted by Ellis Spice on August 27th, 2014 [ permalink ] | Read more »
Foodie Yama Review
Foodie Yama Review By Jennifer Allen on August 27th, 2014 Our Rating: :: BRIEFLY HOOKSUniversal App - Designed for iPhone and iPad Foodie Yama will draw you in for a brief while, and you’ll never be entirely sure why.   | Read more »
Spotify Connect Turns One, Now Supports...
Spotify Connect Turns One, Now Supports New Devices Posted by Ellis Spice on August 27th, 2014 [ permalink ] Universal App - Designed for iPhone and iPad | Read more »
The Rise of PicsaStock and How You Can M...
We all take plenty of photos, right? That’s the joy of having a reasonably powerful camera in your pocket, thanks to your trusty iPhone and a bevy of similarly useful apps. Wouldn’t it be great to make some money out of those snaps? While your... | Read more »
Appointment With F.E.A.R Review
Appointment With F.E.A.R Review By Jennifer Allen on August 27th, 2014 Our Rating: :: CAMP DELIGHTUniversal App - Designed for iPhone and iPad Ever wanted to be a superhero? Appointment With F.E.A.R is a fine way to live that dream... | Read more »
Indie MOBA ÆRENA’s 2.0 Summer Update Bri...
Indie MOBA ÆRENA’s 2.0 Summer Update Brings New Missions, Skins, and a Champion Posted by Jessica Fisher on August 27th, 2014 [ permalink | Read more »
Ultra Drift Review
Ultra Drift Review By Rob Thomas on August 27th, 2014 Our Rating: :: ULTRA DULLUniversal App - Designed for iPhone and iPad Ultra Drift is fine in concept, but the execution runs head-first into the wall. And so will you. Over and... | Read more »

Price Scanner via MacPrices.net

The iPad’s Real Competitive Challenger (Not S...
It’s been my contention for some time that the iPad is suffering from something of an identity crisis, and I suspect that may be a factor in slackening sales this year. Apple can’t seem to decide... Read more
13-inch 2.6GHz/256GB Retina MacBook Pro on sa...
B&H Photo has the 13″ 2.6GHz/256GB Retina MacBook Pro on sale for $1379 including free shipping plus NY sales tax only. Their price is $120 off MSRP. Read more
Life Inventory iOS Apps – Learn to Know Thyse...
James Hollender’s Life Inventory apps s are now on sale with 20% off thru Labor Day, 09/01/2014. This is a great opportunity to get started on that Moral Inventory you’ve been putting off doing for... Read more
Pocket Watch, LLC. Reveals Cloud Server For P...
Beaumont, Texas based Pocket Watch, LLC. has announced the availability of its new ActivePrint Cloud Server Powered by Raspberry Pi. With this small standalone box almost any USB printer or available... Read more
902it Simplifies Area Code Changes For Nova S...
The east coast Canadian provinces of Nova Scotia and Prince Edward Island are phasing in 10 digit telephone dialing, to be fully in place by November, in order to accommodate a second area code to... Read more
Boomerang iPad Stand Mounts Your iPad Anywher...
Boomerang, a Mountable Stand with Multiple Viewing Angles, is now available for iPad Air. Boomerang combines several functions that aim to expand your iPad’s potential in one, elegant product. The... Read more
Retina MacBook Pros available starting at $10...
The Apple Store has Apple Certified Refurbished 13″ and 15″ MacBook Pros available starting at $929. Apple’s one-year warranty is standard, and shipping is free: - 13″ 2.5GHz MacBook Pros (4GB RAM/... Read more
Apple 27-inch Thunderbolt Display (refurbishe...
The Apple Store has Apple Certified Refurbished 27″ Thunderbolt Displays available for $799 including free shipping. That’s $200 off the cost of new models. Read more
Apple offers free $25 iTunes gift card with p...
The Apple Store is offering a free $25 iTunes Gift Card with the purchase of a $99 Apple TV for a limited time. Shipping is free. Read more
Apple’s 2014 Back to School promotion: $100 g...
 Apple’s 2014 Back to School promotion includes a free $100 App Store Gift Card with the purchase of any new Mac (Mac mini excluded), or a $50 Gift Card with the purchase of an iPad or iPhone,... Read more

Jobs Board

*Apple* Retail - Multiple Positions (US) - A...
Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, you're also the Read more
Senior Event Manager, *Apple* Retail Market...
…This senior level position is responsible for leading and imagining the Apple Retail Team's global event strategy. Delivering an overarching brand story; in-store, Read more
*Apple* Solutions Consultant (ASC) - Apple (...
**Job Summary** The ASC is an Apple employee who serves as an Apple brand ambassador and influencer in a Reseller's store. The ASC's role is to grow Apple Read more
Project Manager / Business Analyst, WW *Appl...
…a senior project manager / business analyst to work within our Worldwide Apple Fulfillment Operations and the Business Process Re-engineering team. This role will work Read more
Position Opening at *Apple* - Apple (United...
…customers purchase our products, you're the one who helps them get more out of their new Apple technology. Your day in the Apple Store is filled with a range of Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.