View Source Front Page JavaScript / DHTML Java Language Netscape Servers Open Standards and Architectures E-Commerce Human Interface Case Studies Third-Party News

Error-Handling Techniques for Web Applications

By JJ Kuslich

Send comments and questions about this article to View Source.   

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.

DETECTING ERRORS

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.

User-Generated Errors

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.


Example 1

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.


Example 2
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;
}

Database Errors

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.

Error Levels

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.

HANDLING ERRORS

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 an Error Page

One way you can handle error reporting is to redirect users to a special page,
error.html, whenever your application detects an error. This special page reports that an error has occurred and gives details about the error and possibly tips on how to resolve it. For example, suppose a form in your application has several required fields, and you want to ensure that they're all filled in before accepting the information. If your form handler detects that some of the required fields are blank, it can redirect the user to a page that says some of the fields haven't been filled in and possibly lists the blank fields. Of course, you can first perform any number of other actions, such as closing a connection to a database that you might have opened to insert a record.

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 Multiple Values in a Result Object

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.


Example 3
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.


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;

Creating an Error Object

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.


Example 5
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.


Example 6
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.


Example 7
// 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.)


Example 8
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();
}

Cleaning Up with CleanRedirect()

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.


Example 9
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.


Example 10
// 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);

CONCLUSION

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.


FURTHER RESOURCES


View Source wants your feedback!
Write to us and let us know
what you think of this article.

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.

(8.98)

For the latest technical information on Sun-Netscape Alliance products, go to: http://developer.iplanet.com

For more Internet development resources, try Netscape TechSearch.


Copyright © 1999 Netscape Communications Corporation.
This site powered by: Netscape Enterprise Server and Netscape Compass Server.