You know when you violate the integrity of 4tH, it will exit and report the cause and location of the error. Wouldn't it be nice if you could catch these errors within the program? It would save a lot of error- checking anyway. It is quite possible to check every value within 4tH, but it takes code and performance, which makes your program less compact and slower.
Well, you can do that too in 4tH. And not even that, you can trigger your own errors as well. This simple program triggers an error and exits 4tH when you enter a "0":
: input# \ get a number begin refill drop ( --) bl word number ( n ) dup (error) <> ( n f ) dup 0= ( n f -f ) if swap drop then ( f ¦ n f ) until ( input routine ) ; \ get a number \ if non-zero, return it \ if zero, throw exception : could-fail ( -- n) input# dup 0= if 1 throw then ; \ drop numbers and \ call COULD-FAIL : do-it ( --) drop drop could-fail ; \ put 2 nums on stack and \ execute DO-IT : try-it ( --) 1 2 ['] do-it execute ." The number was" . cr ; \ call TRY-IT try-it
"TRY-IT" puts two numbers on the stack, gets the execution token of "DO-IT" and executes it. "DO-IT" drops both numbers and calls "COULD-FAIL". "COULD-FAIL" gets a number and compares it against "0". If zero, it calls an exception. If not, it returns the number.
The expression "1 THROW" has the same effect as calling 'QUIT'. The program exits, but with the error message "Unhandled exception". You can use any positive number for 'THROW', but "0 THROW" has no effect. This is called a "user exception", which means you defined and triggered the error.
There are also system exceptions. These are triggered by the system, e.g. when you want to access an undefined variable or print a number when the stack is empty. These exceptions have a negative number, so:
throw -4
Will trigger the "Stack empty" error. You can use these if you want but we don't recommend it, since it will confuse the users of your program.
You're probably not interested in an alternative for 'QUIT'. Well, 'THROW' isn't. It just enables you to "throw" an exception and exceptions can be caught by your program. That means that 4tH won't exit, but transfers control back to some routine. Let's do just that:
: input# begin refill drop ( --) bl word number ( n ) dup (error) <> ( n f ) dup 0= ( n f -f ) if swap drop then ( f ¦ n f ) until ( input routine ) ; : could-fail ( -- n) input# dup 0= if 1 throw then ; : do-it ( --) drop drop could-fail ; : try-it ( --) 1 2 ['] do-it catch if drop drop ." There was an exception" cr else ." The number was" . cr then ; try-it
The only things we changed is a somewhat more elaborate "TRY-IT" definition and we replaced 'EXECUTE' by 'CATCH'.
'CATCH' works just like 'EXECUTE', except it returns a result- code. If the result-code is zero, everything is okay. If it isn't, it returns the value of 'THROW'. In this case it would be "1", since we execute "1 THROW". That is why "0 THROW" doesn't have any effect.
If you enter a nonzero value at the prompt, you won't see any difference with the previous version. However, if we enter "0", we'll get the message "There was an exception", before the program exits.
But hey, if we got that message, that means 4tH was still in control! In fact, it was. When "1 THROW" was executed, the stack- pointers were restored and we were directly returned to "TRY-IT". As if "1 THROW" performed an 'EXIT' to the token following 'CATCH'.
Since the stack-pointers were returned to their original state, the two values we discarded in "DO-IT" are still on the stack. But the possibility exists they have been altered by previous definitions. The best thing we can do is discard them.
So, the first version exited when you didn't enter a nonzero value. The second version did too, but not after giving us a message. Can't we make a version in which we can have another try? Yes we can:
: input# begin refill drop ( --) bl word number ( n ) dup (error) <> ( n f ) dup 0= ( n f -f ) if swap drop then ( f ¦ n f ) until ( input routine ) ; : could-fail ( -- n) input# dup 0= if 1 throw then ; : do-it ( --) drop drop could-fail ; : retry-it ( --) begin 1 2 ['] do-it catch while drop drop ." Exception, keep trying" cr repeat ." The number was " . cr ; retry-it
This version will not only catch the error, but it allows us to have another go! We can keep on entering "0", until we enter a nonzero value. Isn't that great? But it gets even better! We can exhaust the stack, trigger a system exception and still keep on going. But let's take it one step at the time. First we change "COULD-FAIL" into:
: could-fail ( -- n) input# dup 0= if drop ." Stack: " depth . cr 1 throw then ;
This will tell us that the stack is exhausted at his point. Let's exhaust is a little further by redefining "COULD-FAIL" again:
: could-fail ( -- n) input# dup 0= if drop drop then ;
Another 'DROP'? But wouldn't that trigger an "Stack empty" error? Yeah, it does. But instead of exiting, the program will react as if we wrote "-4 THROW" instead of "DROP DROP". The program will correctly report an exception when we enter "0" and act accordingly.
This will work with virtually every runtime error. Which means we won't have to protect our program against every possible user-error, but let 4tH do the checking.
We won't even have to set flags in every possible colon- definition, since 4tH will automatically skip every level between 'THROW' and 'CATCH'. Even better, the stacks will be restored to the same depth as they were before 'CATCH' was called.
You can handle the error in any way you want. You can display an error message, call some kind of error-handler, or just ignore the error. Is that enough flexibility for you?