Throwing an exception is the canonical choice for errors which occur in a lower layer (like the DAL), but need to be handled in a way which depends on the context of the higher layers (like the decision about what "a descriptive message" is or how it should be displayed to some user).
However, an exception may not be appropriate when you just want to signal a warning, but still want to return an operational result from a function. For example, when your "1000 row result cap" was met, you may want to return the 1000 objects found so far. If you think of a function call like
List<MyObject> result=dal.SelectObjects(someCondition);
then by throwing an exception the function cannot return the partial result of that operation. So for this cases, an additional error message may be more appropriate, like this:
List<MyObject> result=dal.SelectObjects(someCondition, out someErrorObject);
This works whenever it is fine to get the error messages or warnings at the end of the operation. (Of course, you could also combine the function result and the error information into one object, as shown in Jon Raynor's answer).
There are situations where you need to return warnings or information to the user already during the running operation, not just at the end. For example, let us assume your operation can detect when the query delivers more than 1000 objects, but you don't want to stop the operation then, but to send a signal back to the UI whenever the next 1000 objects (or a certain number) are processed - for example, to trigger a progress indicator. Then you could use a delegate to return information from the DAL to some higher layer, with this signature:
// "callback" will be called with the number of objects processed in some interval
List<MyObject> SelectObjects(string someCondition, Action<int> callBack)
Of course, this looks a little bit contrieved. A more natural design choice for this case is to utilize delayed evaluation using an IEnumerable
:
IEnumerable<MyObject> SelectObjects(string someCondition)
{
// some loop here, using "yield" to return the result
}
Now callers can decide by themselves whether the operation shall be stopped after 1000 objects, or after 10 seconds, or if the user should be asked interactively in this case, if the operation shall just be continued, or if something different shall happen in between.
So as you see, there are several ways of keeping the DAL completely UI or BL agnostic, nevertheless returning error conditions, warnings, or intermediate information back to the higher layers. You just have to pick which of those techniques are appropriate for your case.