Agile Zone is brought to you in partnership with:

I am a programmer and architect (the kind that writes code) with a focus on testing and open source; I maintain the PHPUnit_Selenium project. I believe programming is one of the hardest and most beautiful jobs in the world. Giorgio is a DZone MVB and is not an employee of DZone and has posted 635 posts at DZone. You can read more from them at their website. View Full User Profile

Errors: part of the learning curve

09.17.2012
| 3590 views |
  • submit to reddit

The learning curve of a programming language also consists of its errors (and exceptions), not only of its features. Often we are tempted to estimate a task assuming that everything will go well, but much time can be lost in debugging and finding out why an error happens or a language exception is raised.

Don't think so? Take a look at this Clojure error:

Exception in thread "main" java.lang.ClassCastException:
 clojure.lang.PersistentList cannot be cast to clojure.lang.Symbol,
 compiling:(/home/giorgio/code/fizzbuzz-clojure/fizzbuzz.clj:9)

It meant there was a typo in my code.

We can reduce debugging time via unit testing to catch bugs early, or by designing decoupled components to simplify the big picture; even with stricter type checks to prevent some wiring errors. But when an error comes up, it's time to dig into it and learn more about the language and platform (or even framework) you're standing on.

I will try to list some language agnostic tips for dealing with errors; clarification example will stick to PHP as it's the language whose errors I know better (this is true of every language you pass much time with.)

Locating errors

When you are unfamiliar with a language it is handy to reduce the TDD step to the single line. If this is a situation that sounds strange to you, consider instead being unfamiliar with a framework, an external application like a database, or some message-oriented middleware. When an error comes up, it's not immediate to understand what happened, depending on how mature your drivers are or how much magic or configuration the frameworks contains.

Even a simple error can be daunting if you do not immediately know what caused it. "what" may not be a component: it can be the contract between two of the pieces that make up your application. It may not be the case that MySQL is losing records (unlikely) but that a couple of insert and read queries are not consistent with each other.

To debug errors at the syntax or procedural level, try to iteratively:

  • run a comprehensive test case.
  • Check that no errors have been raised (a test can fail because an expectation is not satisfied, but no error or exception should be logged or shown in this step.)
  • Add a line or a statement.

When you get a new error, you have found the culprit. A long time ago, it was customary to write 1,000 lines of code before running it; now we iterations of 10 seconds are the norm for me. Some developers even run unit tests all the time, at each source file modification or every 2 seconds.

Consider this initial state of one of your methods:

$result = 1;

After a modification, it may look like this:

$result = 1 + $array['field'];

If this change causes an error, either the + operator isn't working or $array does not contain 'field' (or it can't be accessed like this because it's an object, or we are using the wrong syntax). Even a simple step like this can fail for 4 different reasons then. Try this instead:

$result = 1 + 4;

then

$result = $array['field'];

and then

$result = 1 + $array['field'];

and stop whenever there is trouble.

TDD does not always need to be so fine-grained that each line requires to rerun the current driving tests; the size of each step is a function of your confidence (in a direct correlation). The kind of small transformations you need to perform when you're encountering a dubious error is similar to refactoring: in this case you're changing the shape of the code to use different syntax, constructs or collaborations while keeping the same behavior.

The good thing is that with a good unit test suite running this code a thousand times has no overhead other than pushing a button.

Logging errors

Your (testing) framework should be able to gather every exception that is generated during a run, while compile-time errors will interrupt the test before it can be run. It should show you:

  • an error message.
  • The line that generated it.
  • The full stack trace.

If it doesn't show you this, change framework. I wonder if this also applied to frameworks for production code...

Logging errors means that even if an exception bubbles up, it will be logged on a separate file by the interpreter or the virtual machine. This is interesting when running end-to-end tests as there are many layers that can swallow an exception between a fault and the endpoints where you are issuing commands and checking results.

Consider a user interface: its job is to hide technical errors to maintain a smooth user experience. So it's normal to not be able to look at exceptions through the UI; nevertheless, logging can save you while running manual or automated end-to-end tests as everything that goes wrong is stored in a text file instead of just elling you "Oops! Come back later."

For example, PHP's log_errors directive tells the interpreter to write all errors to a separate file, without contaminating the output of PHP scripts with them; at the same time, this file will contain a list of what is going wrong at the language level (missing variables, undefined constants, or deprecated and dangerous features).

Understanding errors

Don't assume that just because you are reading an error message you are able to debug it; understanding an error means first having the means to reproduce it, and then locating which class or method isn't doing what you expect from it.

For example, the real source of an error can be far from where it is generated: just think of a dictionary with missing values. An error is generated when you access one of these values expecting it to be there, but the real problem lies in who has created the dictionary or modified it.

These actions at a distance can be minimized by our own paradigm (an object that wraps the array and intercepts all modifications) or principles (reducing the usage of global structures), but especially when you're relying on the platform/framework/database they cannot be eliminated.

For example, I once traced an error happening at the end of a PHP process to code that was putting anonymous functions in the session. The session map was serialized at the end of the script and the non-serializable anonymous functions caused a cryptic error.

I think that by their nature these interaction errors cannot be avoided *if you don't know they can happen* (once you are familiar with PHP and $_SESSION, you will naturally avoid to write this kind of code, like you would never try to serialize a database connection again).

Thus like the proverb says, Google and StackOverflow are our friends: I shall never ask for help without googling it first. It's very likely that someone else has already encountered the same problem in another application and blogged about it, you're encouraged to do your homework and be prepared when you're explaining the error to one of your colleagues.

On the other hand, these interaction errors can be avoided if you know that that can happen: the trick is in discovering your false assumptions as quickly as possible and in a scenario where it's cheap to fix them.

The integration tests proposed by GOOS are a way to minimize these external errors: instead of reading documentation, jump into writing automated tests that involve the external code (a database driver, for instance). These tests will verify that `SELECT COUNT(*) FROM user INNER JOIN group ON user.group_id = group.id` really returns the set of groups you want; or that db.users.find().sort({name:-1}) sorts in descending order. Discovering these facts through a user interface or as a bug in a browser would be inefficient and distracting.

Published at DZone with permission of Giorgio Sironi, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)