• Post Reply Bookmark Topic Watch Topic
  • New Topic
programming forums Java Mobile Certification Databases Caching Books Engineering Micro Controllers OS Languages Paradigms IDEs Build Tools Frameworks Application Servers Open Source This Site Careers Other Pie Elite all forums
this forum made possible by our volunteer staff, including ...
Marshals:
  • Campbell Ritchie
  • Tim Cooke
  • Liutauras Vilda
  • Jeanne Boyarsky
  • paul wheaton
Sheriffs:
  • Ron McLeod
  • Devaka Cooray
  • Henry Wong
Saloon Keepers:
  • Tim Holloway
  • Stephan van Hulst
  • Carey Brown
  • Tim Moores
  • Mikalai Zaikin
Bartenders:
  • Frits Walraven

rant on throwing exceptions

 
Ranch Hand
Posts: 688
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
I was reading some code over the weekend from a friend of mine who is studying java. Something really bugged me. The way he is throwing exceptions and the way the API he's using from his TA throws exceptions.
It seems that everything is exception!!! For example: there is an method that check for valid data (user input). It throws exception if data is invalid. My question is, why not just making it returns false when data is invalid.
Exception to me means something unexpect happened, but in today's programming, can you really say invalid user input is an exception? I come from a C/C++/scripting background, I indicates exception by using 0,1 or an error code of some kind. I found that easier to handle and useful. I realize that exception is definitely a good thing in java, but in my opinion, I think most people (sadly, some of the TAs in colleges) think Exception should be used extensively to an extreme scale.
You guys been programming for a long time, GURUS . My question is: how much exception throwing is enough? If a try/catch blocks trying to catch 5 different exceptions, don't you think that's somewhat a bad design geenrally speaking? Why do people stop using boolean as a way of checking/assertion?
All of these are general speaking of course, it might not apply to some cases. I just feel that if my friend is paying 20G a year for an education, at least the school has a responsibility to teach him the right thing.
[ February 25, 2004: Message edited by: Adrian Yan ]
 
Ranch Hand
Posts: 1170
Hibernate Eclipse IDE Ubuntu
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator

in cases where return values make no sense, one uses exceptions. If not you will find that returning 0 must be interpreted as a failure, then the calling method must fail, and instruct its calling method that it failed, and that methods calling method must instruct its calling method it failed, etc...
Exceptions make this easy. And yes, its good to validate the parameters of any public method and throw an exception if the value is unacceptable.
 
Ranch Hand
Posts: 1923
Scala Postgres Database Linux
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
Gilbert:
Of course you could redefine your example, to return an error-code, and have the result in an reference parameter:

but this isn't really a more beauty code - isn't it?
And it's easy for the caller to ignore the return-value:

Adrian:
If you want to know, what went wrong, boolean values aren't a good solution.
Think of saving a file.
There may be an invalid FileName, DiskFull, no write permissions, ...
So you could change the return value from boolean to error_code, or have an error_code variable set, which the caller may query.
But this will get complex, if multiple Threads cause errors.
Exceptions can give detailed information about sourcecode line and reason.
They can't be ignored that easily like boolean returns.
But of course you can misuse exceptions or handle them bad.
You should handle them as early as possible.
If invalid userinput occurs, it might be bad to terminate your application with an unhandled exception.
Often multiple exceptions make code bad readable, not because of bad programmers, but because so many things may fail.
[ February 25, 2004: Message edited by: Stefan Wagner ]
[ February 25, 2004: Message edited by: Stefan Wagner ]
 
Stefan Wagner
Ranch Hand
Posts: 1923
Scala Postgres Database Linux
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
Another issue I forgot:
If you use different APIs, you would have different error-codes from different vendors, everyone defining his own error_codes.
In java, you can easily define a new exception:

Exceptions where introduced in c++ - (perhaps earlier elsewhere) - and the motivation well described by Bjarne Strustroup: 'The c++ Language' and: 'Design and Development of c++' (backtranslated from german).
I'm sure you'll find more and better arguments there.
 
Ranch Hand
Posts: 387
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
Error codes are almost always a bad idea. Remember, the good thing about exceptions is that people using the API are FORCED to know about them and handle them. With error codes, it might not be clear what happened and you also have to do a lot of rewriting the same error-code checks wherever you use it. Let's say you have an API and you haven't touched it in a year. How are you going to know right off the bat, all the error-codes and what they mean and when they're raised? If it's declared exceptions on the other hand, it's more clear.
 
Ranch Hand
Posts: 1873
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
Hi all,
I agree strongly with idea of using exceptions than error codes. Why? because I also once wanted to see why the hack we have to use exception. Why we can't use error codes/true/false things to return but it exactly put us into issues of tracking down problems as mentioned by various people here.
I wrote the api that I was gonna use and some other in the company using return values and now I realize that I would have to modify it to throw exceptions intead. Like suggested earlier, if there is a method call ladder you want to bubble up the errors then we really endup having many if...else just to check the error code and return to pass the error up which makes the code difficult/cumbersome to write and annoying to read. Also, this approach of errorcodes would not help us pass up the descriptive error message that we got. Either we have to return some string as error codes having some convention of "if starts with ERROR then error..." to distinguish b/w the successful and failuer return codes OR we lose the ability to return the decriptive errors.
My 2 cents.
Regards.
Maulin.
 
Author and all-around good cowpoke
Posts: 13078
6
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
Back when I was writing code in C++ I sometimes found that half the code was involved in detecting an error code from some subroutine and propagating it back up the calling chain - frequently changing the convention. I love being able to rely on the exception mechanism.
Not having to figure out what error code goes with which method call is a BIG help in designing your classes. The ability to include debugging information in your custom Exception classes can be a real lifesaver.
Bill
 
Ranch Hand
Posts: 268
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
I think using exceptions and validation properly is insanely important, because every significant application has to do both and if they botch it you can wind up with a nightmare situation in the not-too-far-off future.
I've always ascribed to the Design by Contract (DbC) rules for exceptions (well, "always" since I learned about them about two years ago ). Under this mantra, when writing a new method you are free to prescribe any precondition for calling that method you like. You simply have to document it in a way that the caller can use. For example, now when I write javadocs for a method that has a precondition, I'll put it in code so they can just cut'n'paste the check. For example, if I write foo(Object o) and the precondition is that o must be non-null, I'll literally put in the javadoc:

Preconditions:
o != null


Then it's the caller's responsibility to meet that precondition, and if they don't, the behavior of the method is undefined. At my last company I had to drive that point home by presenting a demo class that did *very* unexpected things if the precondition wasn't met (it would randomly select between System.exit(), while(true) { int x=0; x++; x--; }, and other nifty behaviors). Of course, in production code the right thing to do was simply make sure the method begins by asserting the precondition and make sure there's a top-level handler for AssertionErrors.
Now maybe you don't want to specify that every method takes a non-null value because it's the general rule that callers should not pass in nulls. Ok, then you get into the more complex topic of determining "frames" for your classes that define these sorts of general rules...this is interesting but a bit off-topic, so let me un-digress.
I also like the idea of defining postconditions, and even asserting those at the end of a method if practical, though this was sometimes more difficult to do as a rule than preconditions. Even in cases where it wasn't possible to do either, though, the whole idea of when to throw exceptions becomes clear if you think of your methods in this way.
The rule is: if the caller meets all of the prescribed preconditions, yet the method still cannot meet the postconditions due to unforeseeable circumstances, it should raise an exception that characterizes the problem as exactly as is practical.
I too came up against the architect that designed void validation methods. This was work for a telecom company, and there was a long, drawn out process for provisioning all the lines and setting up all the circuits to hook a up a customer with service. At the end of the process, a short validation algorithm was run to ensure that all of the data across all of the databases was consistent and nothing went wrong. This validate method took the customer's ServiceAccount instance and threw exceptions if problems were found.
Now look at this validate method in terms of the DbC rules I laid out. Assume the only precondition is the supplied ServiceAccount object cannot be null...the caller meets that, but still has to beat back a wide array of exceptions that could be raised by the method. Ridiculous. Instead, the method should've returned a boolean saying whether it was valid or not. Oh, the architect argued, but I want this method to report the nature of the problem, too. Ok, then, have the method return a validation status object that describes the validation status of the account. Problem solved. But there's no reason to be unrolling stack traces...you're calling a validation method, and it should not be considered an "exceptional" case if the validation method fails. Built into the idea of performing validation is that it will sometimes fail--otherwise, what's the point in doing validation?
I've never had a problem using this approach. If you always force the caller to agree that the state of the system conforms to some specific preconditions, you can agree to provide the caller with some service. If circumstances arise where, despite the caller meeting the preconditions, you still cannot perform the expected service, go ahead and raise exceptions.
sev
 
Mr. C Lamont Gilbert
Ranch Hand
Posts: 1170
Hibernate Eclipse IDE Ubuntu
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
I make a practice of only validating the public methods though. I use assert on the non-public methods.
 
Ranch Hand
Posts: 1608
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
You are right - declaring to throw java.lang.Exception is fugly, and declaring to catch Exception makes my stomach churn. It also has a tendency to make my blood boil when I have to pay the consequences of such an obscene act (such as using a third party API or an application that does it).
</bodily-functions>
In your description, you don't mention the differentiation between checked and non-checked exceptions. This distinction is an understanding that is fundamental to your argument. In general, a checked exception indicates a recoverable error, such as an I/O error (@see java.io.IOException) and a non-checked exception indicates an unanticipated programmer error.
For this reason, it is generally (*see Foot Note) poor form to catch non-checked exceptions (and even worse to catch java.lang.Exception).
http://java.sun.com/docs/books/tutorial/essential/exceptions/
*Foot Note: There may be reasons why you would need to catch a non-checked exception, but it should be done having full knowledge of the consequences of doing so.
 
Adrian Yan
Ranch Hand
Posts: 688
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
Wow, alot of good response. Just to clear something, I'm not against throwing Exceptions. I use it as well. I think that to me, it's more of how we should use it.
I'm not a language guru or a PHD in CompSci, but from my coding experience, I found Exception model is abit flawed. Most of us don't catch RuntimeException, but from reading error and exception from programs and libraries, NullPointerException is probably the most common one. Of course, this should never happened (in theory), but it does happened, and happens alot more than I think it should be.
If someone tells me RuntimeException is something JVM should handle, and not the responsibility of the programmer, I would laugh my ass off. If a customer uses my software, and he gets an error, and calls me, imagine the look on his face when I say, "errr... sorry dude, it's not my problem, it's Sun's problem!"
 
Ranch Hand
Posts: 585
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator

Originally posted by Adrian Yan:
Wow, alot of good response. Just to clear something, I'm not against throwing Exceptions. I use it as well. I think that to me, it's more of how we should use it.
I'm not a language guru or a PHD in CompSci, but from my coding experience, I found Exception model is abit flawed. Most of us don't catch RuntimeException, but from reading error and exception from programs and libraries, NullPointerException is probably the most common one. Of course, this should never happened (in theory), but it does happened, and happens alot more than I think it should be.
If someone tells me RuntimeException is something JVM should handle, and not the responsibility of the programmer, I would laugh my ass off. If a customer uses my software, and he gets an error, and calls me, imagine the look on his face when I say, "errr... sorry dude, it's not my problem, it's Sun's problem!"



That's not what is meant by RuntimeException being a JVM exception. What it means is that a RuntimeException should indicate something that happened at a JVM level. It is an error that is NOT an application error (e.g. not a calculation error) and is something that even if caught, makes the application unable to properly continue.
 
Adrian Yan
Ranch Hand
Posts: 688
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator

It is an error that is NOT an application error (e.g. not a calculation error) and is something that even if caught, makes the application unable to properly continue.


Understand, but I don't believe it's true in all cases. ArithmeticException, or the "divide by zero", while it's definitely an error (runtime), why would it stop the application from continuing?
 
Tony Morris
Ranch Hand
Posts: 1608
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator

imagine the look on his face when I say, "errr... sorry dude, it's not my problem, it's Sun's problem!"


A perfect example of why catching any java.lang.RuntimeException "should be done having full knowledge of the consequences of doing so." It appears in this case, that you aren't aware of these consequences.
A java.lang.NullPointerException is NOT an indication that "it's Sun's problem". In fact, it is solely an indication that it is the application developer's problem, since it should never have occurred in the first place. The application developer did not anticipate that behaviour of their application (dereferencing of a null reference). The same can be said for ArithmeticException - this *should* never occur, but if it does (since it was unanticipated), you (the application developer) have screwed up.
I can only suggest you read the exceptions tutorial to clarify what appears to be a misunderstanding.
http://java.sun.com/docs/books/tutorial/essential/exceptions/
 
Adrian Yan
Ranch Hand
Posts: 688
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
I don't understand the last post on RuntimeException. NullPointerException or IndexOutOfBound or Arithmetic exceptions are RuntimeException, therefore, compiler does not require it to be declare. This is where I believe causing a lot of problem. The whole idea of only programmers cause exceptions are clearly wrong. There are many instances where the user performs invalid operations (I.E. invalid data, or other programmer using your API can passed a Null object by mistake).
 
author
Posts: 154
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
It may not be in code that you wrote yourself, as you say, another programmer using your class may be passing a null to one of your methods by mistake. It's still a programming bug though.
If the user is able to enter free-format data, you must write code to check that the data is valid before using it. If your program throws a RuntimeException, it means a contract somewhere has been broken and the code needs to be fixed.
 
(instanceof Sidekick)
Posts: 8791
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
I've seen several experts argue that we should only use unchecked exceptions to keep the code clean. I tried this in one project, my Wiki. I catch any checked exceptions immediately and throw a WikiException. WikiException extends RuntimeException and references the original exception so I can display the original stack trace as well as my own. It worked very nicely because the call stack can get pretty long with recursion and all, yet only the top level class that's managing the request can actually do much about any errors. It cannot throw exceptions because it's on its own thread and there is nobody to catch them. So it catches everything.
I'm not sure if I'll use this again. It worked well this time because most classes could not do anything useful in response exceptions.
 
David Peterson
author
Posts: 154
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
Exceptions are just a way to pass information about.
RuntimeExceptions indicate there is a bug your code which needs to be fixed.
Using a RuntimeException just to "keep the code clean" is not a good idea, as you then lose the distinction.
 
Adrian Yan
Ranch Hand
Posts: 688
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
I don't like to extend RuntimeException unless I can't figure out anything better. I have to say, I catch all possible exceptions, not by single catch (Exception e), but by individual possible exceptions, including Null, Arithmetic.
IMHO, Exception, regardless of checked or unchecked exceptions, need to be handled. I personally think that in some cases, using error code or boolean as a checking/assertion mechnanism works very well if not better than simple blindly create new exceptions. My philosophy is KISS (Keep It Simple Stupid).
While I try to be perfect in all aspects of coding, I'm 100% sure that my code will have errors, and exceptions and bugs that I don't know about. So the whole idea of anticipating all exceptions to me is ridiculous if not impossible. That's why catch all exceptions are important in my program.
 
Stefan Wagner
Ranch Hand
Posts: 1923
Scala Postgres Database Linux
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator

Originally posted by Adrian Yan:
I don't understand the last post on RuntimeException. NullPointerException or IndexOutOfBound or Arithmetic exceptions are RuntimeException, therefore, compiler does not require it to be declare. This is where I believe causing a lot of problem.


If every array-usage and every object-Usage could throw an Checked-Exception, code would get unreadable, and you could stop programming.
Since Exceptions are Objects too, you would get a recursive, endless exception-throwing cascade
 
author
Posts: 361
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator

Originally posted by Adrian Yan:
I don't like to extend RuntimeException unless I can't figure out anything better.


Gentlemen:
I rarely exend RuntimeException, but I can give you an example of a time a couple of years ago when I did. I was consulting at a large financial institution in the midwest. We had implemented a five layer architecture, and there were erroneous conditions being identified in the object-relational mapping code in one of the lower layers. In order to redirect to a nice JSP error page we wanted to act on the errors in a higher layer. There were many classes in the layers in between. A checked exception would have broken a lot of code. This would have resulted in a lot of modifications to existing classes and the development schedule was tight (as usual). After discussing my ideas with the team, we decided to try creating two custom exceptions as subclasses of RuntimeException. It worked great and saved us from having to do a lot of rework. So that's my two cents.
Regards,
 
Stan James
(instanceof Sidekick)
Posts: 8791
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator


RuntimeExceptions indicate there is a bug your code which needs to be fixed.
Using a RuntimeException just to "keep the code clean" is not a good idea, as you then lose the distinction.


David, I'm not picking on you because I haven't solidly determined my own position on this, but be aware there are plenty of good brains who disagree. They say that the decision to build checked exceptions into Java was, in retrospect, a mistake from day one and other languages that have everything unchecked work out better in the long run. Using exceptions derived from RuntimeException let you write Java more like those other languages. My own experiment with it was quite satisfying, but that may have been a quirk of the particular application design.
 
David Peterson
author
Posts: 154
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
Do you have a reference to support this? I'm interested to see their arguments in full.
As I see it, methods generally perform some function and return results. The person calling the method needs to know what parameters the method takes and what results to expect. An "exception" is one kind of result.
Using checked-exceptions means you must document and handle exceptions explicitly. You cannot ignore a checked exception. You must either handle it, or throw it yourself. The programmer can't say "Oh, I didn't realise that could happen".
Yes, many languages don't have checked-exceptions, and many languages aren't strongly-typed. These may be great for knocking up quick scripts, but for serious applications, where production problems are expensive, you really want to use any tool available to you to catch bugs as early in the lifecycle as possible.
Checked exceptions help by making each method's "contract" more explicit.
 
David Peterson
author
Posts: 154
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
All right, I've found a reference: Bruce Eckel believes that "... checked exceptions encourage people to make them vanish. Plus they make much less readable code."
These two arguments are not really against checked-exceptions per se, but simply that inexperienced programmers find them hard to understand and end up doing bad things like swallowing exceptions with empty catch clauses.
In this case the debate is about whether the language should be dumbed-down for the sake of beginners.
 
Ranch Hand
Posts: 580
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
Checked exceptions don't make code much less readable IMHO.
Would it be possible to have a special annotation where :

gets compiled to code equivalent to :

It could be more sophisticated too so you can declare to ignore specific exception types.
This would make the code more readable and solve the swallowed exception problem.
D.
[ March 02, 2004: Message edited by: Don Kiddick ]
 
blacksmith
Posts: 1332
2
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator

Originally posted by Stan James:
David, I'm not picking on you because I haven't solidly determined my own position on this, but be aware there are plenty of good brains who disagree. They say that the decision to build checked exceptions into Java was, in retrospect, a mistake from day one and other languages that have everything unchecked work out better in the long run.


Like others in this thread, I used to think most exceptions should be checked, and I've moved almost to the opposite pole now, so that nearly all the exceptions I use these days are unchecked. I also use them only to indicate programming errors - usually calling code that failed to obey the calling contract for a function - so I just use Java's built in runtime exceptions (like IllegalArgumentException) without further extending them.
However, there are specific cases where I think checked exceptions are the right way to go. JDBC is a good example. The caller has no way of knowing when attempting to commit a database transaction whether the commit will succeed, or will fail due to another thread's modification to the data. Since the commit can fail even if all the code is correct, a runtime exception that causes the application to terminate with a stack trace is inappropriate. On the other hand, the application should never proceed as if the data was successfully written to the database, because, well, it wasn't - and somebody is going to care about that. Since this is a failure that the caller must always handle, a checked exception is appropriate, and that's what JDBC throws.
Now, I do think that the required syntax for exception handling is ugly, and I think it's a real turnoff for a lot of people, especially those that like left aligned opening braces.

is just a lot more verbose and complex looking than

But, if you think about it, that's not an inherent problem with checked exceptions; it's just a syntax issue. If you eliminated the 'try' keyword - and you could, since the try block is also identified at the other end by the 'catch' keyword - and you eliminated the requirement to use a block, and allowed simple statements, you could have:

which is a lot less offensive looking. For those, like myself, who always use braces after an 'if' statement and would probably do the same with catch blocks, you'd only have to allow simple statements in the try block.
Unfortunately, it's probably a bit late to change the Java language spec that way, even if it wouldn't break any existing code.
 
Ranch Hand
Posts: 196
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
Maybe I'm right and maybe I'm wrong. But the exception debate is very old.
Some months ago Bill Venners did have a couple of interviews with both James Gosling and Anders Hejlsberg. And ofcourse they didn't agree also :
The interview with gosling : http://www.artima.com/intv/solid.html
The interview with heijlsberg : http://www.artima.com/intv/handcuffs.html
Based on those interviews I realized again that there is no right or wrong way to implement exception handling. Maybe there are only wrong ways :-) Otherwise we wouldn't disagree so often :-)
Some years ago I've read the book Programming Language Concepts of Carlo Ghezii and Mehdi Jazayeri. Look over here to find out more about it :
http://www.infosys.tuwien.ac.at/pl-book/
It is really a great book and fun to read. But after reading the book a second time I realized that people like Wirth, Ritchie, Stroustrup, Wall, van Rossum, Gosling, .... all have an opinion. And that it is the language designer that makes a choice. Without right or wrong. So I'm more interested in why they did make a certain choice. And especially why they
designed exception handling as they did.
Is there a book or article ever written about the rationale of exception handling in all different kind of object oriented programming languages?
I would be very interested.
[ March 05, 2004: Message edited by: Arnold Reuser ]
 
So it takes a day for light to pass through this glass? So this was yesterday's tiny ad?
Gift giving made easy with the permaculture playing cards
https://coderanch.com/t/777758/Gift-giving-easy-permaculture-playing
reply
    Bookmark Topic Watch Topic
  • New Topic