So you think you've written the perfect application? Well, if your application works perfectly with perfect input, you're really only half done. Reliable applications must also consider interacting with imperfect users and imperfect external systems, such as database servers. In fact, it's so important for your application to be able to handle bad user input or errors produced by other systems that you must design it to handle errors from the start. You must adjust your notion of what's perfect and build from there.
In the software world, a "perfect" system is one that handles most of the cases most of the time; it's one that asymptotically approaches an error-free system but never quite reaches it. In other words, perfect doesn't mean "error free" but "as error free as possible given time, budget, and other constraints." Don't dismiss this notion of perfection as cynical or pessimistic. It's simply realistic and must be understood before you start designing your application. Otherwise, you could wind up spending too many resources trying to provide complete protection against application Armageddon. Of course, you don't want to swing too far in the other direction either, leaving it up to service packs or point releases of your software down the road to provide sufficient error handling.
Because it's often hard to determine just how much error handling is enough, this article will present several techniques that you can use alone or in combination to provide varying levels of protection. While this article focuses on server-side JavaScript (SSJS) applications, you can use several of the concepts and many of the code samples in client-side JavaScript (CSJS) logic as well. And note that the techniques apply not only to JavaScript development but to Web development in general. You can apply just about all of the error-handling tips to projects in Microsoft's Active Server Pages (ASP) and Sun's new Java Server Pages (JSP), and even to CGI scripts.
Regardless of the level of protection you wish to provide, detecting error conditions is the first step in handling errors. Error conditions are usually generated by users, interactions with external systems, or your own application logic. Let's look at user-generated errors first, then consider database errors, and finally examine the important issue of error levels.
Users can generate errors by entering invalid data, selecting invalid program options, or performing operations out of sequence. You can detect any of these error conditions by validating user input. However, in most cases you'll only want to use validation to detect data entry errors. Overusing validation can be expensive in terms of processing power and can lead to inflexibility in application design because it often ties application logic to the user interface.
For instance, suppose you were to perform some validation each time users select a program option, such as "Edit Employee Records," to make sure that they're allowed to select that option from the page they're on or from the point they're at in some sequence of operations. Validating every one of these operations for every user of the system would require a lot of conditional code and would also effectively hard-wire application sequences and interfaces to the underlying code. It would be far more effective to prevent user interaction errors than to detect them after the fact. From a user interface perspective, it's much more effective to design an interface that guides users through valid choices than to build one that inundates them with error messages every time they select an invalid option. Again, such decisions are fundamental to how you design your user interface and must be made during the design phase of a project.
For data entry errors, validation works well, and in many cases must be performed to prevent bad data from being sent to a critical system, such as a database. Data entry validation involves developing acceptance criteria that identify the type and format of data allowed for each data field. Once the acceptance criteria for all data fields have been determined, you can write routines that check to see if data entered fits the acceptance criteria.
You can perform data validation in the browser with CSJS, on the server in an SSJS page, or to varying degrees in both places. CSJS validation of form fields, for example, gives you the ability to provide immediate feedback to users should they enter data that fails the acceptance criteria. In addition, as you may already know, performing validation on the client side reduces network traffic and server-side processing, giving the appearance of a faster, more responsive application.
Example 1 shows a form field, and its underlying JavaScript code, that accepts only numeric characters and reports an error if the user attempts to submit the form with invalid data.
function validateFields(theForm) {
// Where IsNum() is a custom function that checks a string to see if
// it contains only numbers. if (IsNum(theForm.numbers.value))
theForm.submit();
else
alert("The numbers field contains invalid characters.");
}
Of course, validation doesn't apply just to processing HTML forms. You
can also validate parameters that are passed to any SSJS function, object
constructor, or custom method that you write. For instance, if you can't
trust that the caller of a custom function will pass arguments of the proper
data type, you can use the typeof operator to check the type of
the arguments before attempting to use them in your function. Such simple
validation checks, as shown in Example 2, can save you from crashed applications
and blank screens caused by some yahoo passing in a null where
a string value was expected.
function ReverseString (forwardStr) {
var revStr = "";
// Validate the argument here.
if (typeof(forwardStr) == "string") {
// Without validation, if forwardStr were null, the attempt to access
// the length property would fail and cause the application
// to crash. Because we validate above, we're protected from that
// error. for (var i=forwardStr.length; i >= 0; i--) revStr += forwardStr.charAt(i); } // End if. return revStr; }
No matter how much we like to complain about users, they're not the only players that generate errors. If you access an external system, such as a database server, you should plan for the unexpected. Database connections can mysteriously disconnect, system administrators can trip over power cords, and database servers can have bugs. The SSJS
Connection object has four methods that will help you detect and gather detailed information about database server errors: majorErrorCode, majorErrorMessage, minorErrorCode, and minorErrorMessage.You can call
majorErrorCode after performing a database operation, such as inserting a row in a table, to see if any errors have occurred. If the operation was successful, it returns a value of zero; if an error occurred, it returns a nonzero integer that represents the actual vendor-specific error code. Once you've detected the error, you can call majorErrorMessage to retrieve a related vendor-specific error message that the database server might have generated. Additionally, you can call minorErrorCode and minorErrorMessage to try to find more detailed error information.The codes and messages returned by the four methods can change every time you interact with the database, so you should check for errors immediately after performing an operation that you wish to monitor. Keep in mind when using these methods that not all database servers provide messages for every error, and that
minorErrorCode and minorErrorMessage may not return useful information in many cases.In addition to the four methods mentioned above, many methods in the SSJS database API return error codes that can tell you whether you should look to
majorErrorCode and majorErrorMessage to provide you with more detailed error messages. For more information about detecting database errors from SSJS, see Chapter 12 of the Netscape SSJS developer's guide, Writing Server-Side JavaScript Applications.So far we've discussed errors as if they were a binary condition -- either you have an error or you don't. However, some applications may require more sophisticated error detection than that. You may need to know what kind of error occurred and how severe it was before you can determine how to handle it. For that reason, when you're designing your application you should think about errors into severity levels such as "warning," "input error," and "fatal error."
For instance, in a data entry application, a warning might be used to report to the user that she hasn't filled out any optional fields in an input form, though she'll still be allowed to submit the form. By contrast, an input error could require the user to fill in all mandatory fields before she can submit the form and proceed to the next part of the application.
Differentiating among error levels can also be useful as a debugging tool. For example, you could design your application to output detailed warnings and status conditions while debugging but silence this noncritical feedback during normal execution.
Defining a hierarchy of error levels, codes, and messages can take a lot of planning, especially for large applications. Once again, let me stress that this planning shouldn't take place as an afterthought but rather should be a normal part of any application design process.
Once you've detected an error, you must decide what to do with it. Your options include reporting the error to the user and allowing him to take some action, taking some action (such as rolling back a database transaction) automatically, failing gracefully and then halting the application, or any number of other responses. In most cases, you'll want to let users know what happened and what they can do about it.
Constructing such an error page is fairly straightforward. From the page where the error occurred, you need to send details about the error such as error codes, error messages, and suggestions for resolving the problem. Depending on your application, you might not need to report all of this information, so it's up to you to decide how far you want to go.
One of the simplest ways to send information to the error page is to send your error message as part of the URL query string. When you redirect to your error page, you'll have to provide a URL anyway, so you might as well append a query string and send along an error message. Don't forget to URL-encode the message, using the JavaScript function
escape(), in case it contains spaces, special characters, or punctuation, none of which are allowed unencoded in URLs. For example, the following redirect statement sends the URL-encoded text of an error message to error.html:errMsg = "Some of the required fields are missing.";
errMsg += "Please click the Back button on your browser and try again.";
redirect("error.html?myerror=" + escape(errMsg));
The error page could then dynamically write out the value you sent over,
request.myerror, and possibly provide some static information such
as webmaster e-mail addresses, links to online help, or other contact information.
Keep in mind that SSJS automatically decodes the URL-encoded value of the
myerror property for you before storing it in the Request
object. You can include as many parameters as you like in order to pass
more information, such as error codes and tips on resolving the error.
This method for handling errors works fine in simple cases. Next I'll explain some more robust ways to return errors from your functions.
Returning error information from a page such as a form handler can be a simple matter, but returning an error code or message from a JavaScript function can be tricky, since JavaScript functions can return only a single value or object. What do you do if you need to return both an error message and a return value from the function? Because a single object can hold many values, you can return a custom JavaScript object that contains as many values as you need to return, including error codes and messages. This trick can be used not only for error handling but anytime you need to return multiple values from a function.
Example 3 shows the definition of a typical result object that you can return from your functions. Every application is different, so feel free to modify the object to fit your needs.
function ResultObj (result, errorMsg) {
// Define object properties.
this.result = result;
this.errorMsg = errorMsg;
}To use this object, simply instantiate it in your function, set the errorMsg and result properties, and return the object, as shown in Example 4.
// Assume the variable stockVal contains the number of items currently // in stock and was set prior to calling the following lines of code. var result = new ResultObj(stockVal, "Stock value is below the minimum level."); return result;
Now suppose that you want to enable your result object to return more sophisticated error information than just an error message. You could modify the result object to do this, or you could create a separate object to handle error information. I recommend creating a separate error object and storing an instance of it in your result object, to separate the behaviors of storing and manipulating results from those of storing and reporting errors. Once you separate the duties out into an object, you can think about assigning methods that look up error messages based on error codes returned, and customizing error output.
Example 5 defines a basic error object, which you can modify and expand to suit your needs. Notice that the methods
getErrorCode and getErrorMessage are just basic getter methods and don't do anything special. Later you'll see how you can expand these methods to perform additional operations.
function ErrorObj (errorCode, errorMsg) {
// Define properties of the object.
this.errorCode = errorCode;
this.errorMsg = errorMsg;
// Define methods of the object.
this.getErrorCode = eoGetErrorCode; this.getErrorMessage = eoGetErrorMessage; this.serialize = eoSerialize; this.deserialize = eoDeserialize;
}
function eoGetErrorCode() {
return this.errorCode;
}
function eoGetErrorMessage() {
return this.errorMsg;
}
function eoSerialize() {
var resultStr = "";
// Delimit the code and message values with a semicolon (;).
resultStr = this.getErrorCode() + ";";
resultStr += this.getErrorMessage();
// URL encode the string so it can be added to a URL.
resultStr = escape(resultStr);
return resultStr;
}
function eoDeserialize(serStr, decode) {
var valArray = new Array();
if (typeof(serStr) == "string") {
if (decode)
serStr = unescape(serStr);
// Split the string into two strings using the
// JavaScript split() method of the String object.
valArray = serStr.split(";");
this.errorCode = valArray[0];
this.errorMsg = valArray[1];
}
}
To use the error object effectively you must recognize that as an object,
it has its own behaviors. For instance, notice that the object has a method
called serialize() that "flattens" out the object into part of
a URL-encoded string, which you can then append to a query string and send
to an error page. Example 6 shows how you could use the ErrorObj object
with a form handler to report a database connectivity error.
var error = new ErrorObj(7, "Invalid password");
redirect("errorpage.html?err=" + error.serialize());
Example 7 shows how the error page would reconstitute the ErrorObj
object based on the URL-encoded values and generate the error messages.
You can experiment with this code and try to find the right balance between
using a simple error handler and full-blown error objects.
// Initially construct an empty object.
var error = new ErrorObj();
// Because the Request object automatically decodes its properties,
// simply pass the serialized string contained in request.err to
// the deserialize () function and tell it not to decode it.
error.deserialize(request.err, false);
write("Error code: " + error.getErrorCode() + "<br>");
write("Error message: " + error.getErrorMessage());
The examples above show only one type of error object. In keeping with
languages like Java, you could think of these error objects as more like
exception objects and specialize them to contain custom information and
even methods depending on the type of exception they represent. For instance,
you could create a specialized error object called DBError that
knows how to report specific information about database errors. Instead
of forcing the user to call majorErrorCode and majorErrorMessage,
it could override the standard ErrorObj object's getErrorMessage
method and automatically call majorErrorMessage as part of
its built-in functionality.
To specialize error objects, we'll need a way of determining which type of error object is returned so we know which interface to rely on when we start working with it. For this, we'll add an
exceptionType property and corresponding getExceptionType method. Example 8 shows the definition of such a DBError object, which inherits from the base ErrorObj object. (In case you don't already know it, you can use inheritance with user-defined JavaScript objects.)
function DBError(connObj) {
this.dbConnection = connObj;
this.exceptionType = "DBError";
this.getExceptionType = dbeGetExceptionType;
// Override the ErrorObj getErrorCode and getErrorMessage methods.
this.getErrorCode = dbeGetErrorCode;
this.getErrorMessage = dbeGetErrorMessage;
}
// DBError gets its prototype from the ErrorObj object.
// NOTE: This statement must be executed in an SSJS page, client-side page, or
// client-side .js include file. It will NOT work in an SSJS .js include file.
DBError.prototype = new ErrorObj;
function dbeGetExceptionType() {
return this.getExceptionType;
}
function dbeGetErrorCode() {
// Return a database error code.
return this.dbConnection.getMajorErrorCode();
}
function dbeGetErrorMessage() {
// Return a database error message.
return this.dbConnection.getMajorErrorMessage();
}
Finally, before you leave the page you're on and redirect to an error page, don't forget to perform any cleanup operations that might be needed. In database applications, for example, simply zipping off to an error page without first closing cursors and releasing connections can cause some real problems. However, you certainly don't want to write the same cleanup code several times on every page where you might need to redirect to an error page. Instead, you should write a function that will perform common cleanup tasks and that you can call from anywhere.
Example 9 is a reusable function,
CleanRedirect(), that can close cursors and result sets and release database connections automatically, and then redirect to your error page (or any URL, for that matter) when it's done. This function will close only one cursor and release only one connection, but you can modify it to close several without too much trouble.
function cleanRedirect(url, connObj, cursor, rsObj) {
if (rsObj != null)
rsObj.close();
if (cursor != null)
cursor.close();
if (connObj != null)
connObj.release();
redirect(url);
}
Example 10 shows how to use CleanRedirect() in a typical database
application.
// Assume hrdbPool is a stored DbPool instance that was initialized
// in the application's initial page.
var hrconn = project.hrdbPool.connection("HRConn", 15);
var hrcursor = hrconn.cursor("SELECT * from EMPLOYEES where CITY='Seattle');
// Perform database operations ...
// Ready to close and redirect to a results or error page.
var url = "errorpage.html?err=" + escape("A database error has occurred.");
CleanRedirect(url, hrconn, hrcursor, null);
I hope you'll find the error handling tips and tricks presented in this article as useful as I have in my own projects. Whatever platform you're using, just remember to plan your error-handling mechanisms in the design phase, not at the end of the implementation phase. Designing code that performs a task is only half the battle. Customers want applications that meet their list of functional requirements, but they also want applications that don't fall apart every time someone sneezes.
If you have any error-handling tips or tricks of your own, be sure to post them to the LiveWire newsgroup and share your experience with the community of SSJS developers out there. SSJS is still somewhat raw and immature, with no real server-side exception model to help out, and we all could use a little help now and then.
JJ Kuslich is
a project manager for the Software Industry Services Group at Application
Methods, Inc. Founded in 1986, Application Methods is a software consulting
firm specializing in custom database applications, Web application development,
and Internet commerce. Application Methods was recently acquired and is
now a wholly owned subsidiary of Rocky Mountain Internet, Inc. Meanwhile,
JJ is looking for his own acquisition deal and soon hopes to become a wholly
owned subsidiary of Cute Single Woman, Inc.
For the latest technical information on Sun-Netscape Alliance products, go to: http://developer.iplanet.com
For more Internet development resources, try Netscape TechSearch.