Originally, our architecture relied on returning null on failure. The levels above would check for this and handle it appropriately. There are a few problems with this. The problem that impeded us the most is that we were unable to differentiate between various kinds of failures for some method. In hindsight, this isn't a very clean way to handle error handling in general.
The first solution we tried involved an external error handling class. Our model (and data access code) would set flags in this static error handling class to indicate what went wrong. This meant that every time someone wanted to call on the model to do something, they would have to remember to check the error handling class to see if anything went wrong. This created a coupling between this error handling class and our models. If you forget to check that error flag, your code would fail silently or cause unexpected behavior. We eventually scrapped this idea in favor of exceptions.
Exception were better because any failure would force the programmers to deal with it immediately. If they forgot to catch the exception, the program would crash right away, and with a very clear reason (like SQLException). You could not simply ignore the error and continue running in ignorance like with the other static error handling technique.
After we decided on exceptions, we started talking about whether we wanted checked or unchecked exceptions. More fun debating there. All this happened after 2AM. I am always hesitant to make important design decisions at that hour.
We eventually decided to go with checked exceptions. This means changing almost all of our classes (including all our tests). Granted, the changes are fairly simple and straightforward, but it's still going to cost us some time to re-factor everything. Had we thought about error handling straight away, we could have saved some time.
Moral of the story: Think about error handling during your planning phase!
On an unrelated note, Willis and I got 97 on OS A2. Hell-freaking-Yeah!