Prior to this day I didn't pay much attention to it. However today I read the documentation again and discovered something new about it.
AutoHotkey lacks a lot of things people want in languages. One example of this is its kind of limited features for throwing errors and functions like a printStack function.
And before I discovered this little feature in the exception function I thought it was impossible to do manually.
What is interesting and new to me about this function was the last sentence ( it seems I never properly read this sentence before I dont know why and it seems like many of you are the same so I will make a post here )Helpfile wrote:Exception(Message [, What, Extra])
Creates an object with the following properties, also common to exceptions created by runtime errors:
•Message: An error message or ErrorLevel value.
•What: The name of the command, function or label which was executing or about to execute when the error occurred.
•Extra: Additional information about the error, if available.
•File: Set automatically to the full path of the script file which contains the line at which the error occurred.
•Line: Set automatically to the line number at which the error occurred.
If What is omitted, it defaults to the name of the current function or subroutine. Otherwise it can be a string or a negative offset from the top of the call stack. For example, a value of -1 sets Exception.What to the current function or subroutine and Exception.Line to the line which called it. However, if the script is compiled or the offset is invalid, What is simply converted to a string.
If you set the second parameter to a negative number you can throw more descriptive and accurate errors and even access the call stack.
A) More accurate errors:
If you ever wrote a function or class for someone else and used Throw to make them aware of errors you probably also used the exception function to write something like this:
Code: Select all
MyReuseableFunc( nr, min, max ) ;For this example our function will take a number and put it between the minimum and maximum numbers given to it
{
if !( ( nr|1 ) && ( min|1 ) && ( max|1 ) ) ;this checks if all parameters are numbers
Throw exception( "At least one parameter is not a number" )
if ( min >= max )
Throw exception( "min needs to be smaller than max" )
;...
}
Code: Select all
MyReuseableFunc( nr, min, max ) ;For this example our function will take a number and put it between the minimum and maximum numbers given to it
{
if !( ( nr|1 ) && ( min|1 ) && ( max|1 ) ) ;this checks if all parameters are numbers
Throw exception( "At least one parameter is not a number" )
if ( min >= max )
Throw exception( "min needs to be smaller than max" )
;...
}
MyReuseableFunc( 10, 12, 13 )
MyReuseableFunc( 10, 12, 13 )
MyReuseableFunc( 10, 0x12, 13 ) ;error
You might have noticed that in the error i acts as if the inside of the function is the problem - it doesn't point to the bad function call.
That's not very descriptive - if you have a function that you use a 100 times in a few 100 different places and you don't know which call caused the issue.
And here is where the exception function steps in or rather it's feature to go upwards in the stack:
Code: Select all
MyReuseableFunc( nr, min, max ) ;For this example our function will take a number and put it between the minimum and maximum numbers given to it
{
if !( ( nr|1 ) && ( min|1 ) && ( max|1 ) ) ;this checks if all parameters are numbers
Throw exception( "At least one parameter is not a number", -1 )
if ( min >= max )
Throw exception( "min needs to be smaller than max", -1 )
;...
}
Code: Select all
MyReuseableFunc( nr, min, max ) ;For this example our function will take a number and put it between the minimum and maximum numbers given to it
{
if !( ( nr|1 ) && ( min|1 ) && ( max|1 ) ) ;this checks if all parameters are numbers
Throw exception( "At least one parameter is not a number", -1 )
if ( min >= max )
Throw exception( "min needs to be smaller than max", -1 )
;...
}
MyReuseableFunc( 10, 12, 13 )
MyReuseableFunc( 10, 12, 13 )
MyReuseableFunc( 10, 0x12, 13 ) ;error
Use this to make your errors more accurate.
You can even add a -2 if the error probably lies 2 calls above your current function and many other things.
B) Print Stack:
Printing the stack is very important information when debugging difficult bugs.
You might have 50 functions calling 20 functions which then in turn also call 1 function.
And under rare specific circumstances an error occurs while calling that 1 function.
And no matter how good you make your throws you will probably never get the entire story with just that - so you start to debug manually or with break points.
That just takes hours and you might even miss the specific point where the error occurs.
So you start to work with MsgBoxes to see which part executes and which doesn't to see where exactly you are when the error occurs.
That also takes time. It would be much easier if you could see where you are when the error occurs meaning could seeing which code called which code etc.
In many languages a printstack function is used for exactly that. However in AHK we do not have such a function.
So what we do know is built our own:
Code: Select all
printStack( offset := -2 ) { ;turns the results of the generateStack function into human readeable text
stack := generateStack( offset )
str := "Stack Print, most recent calls first:`n"
for each, entry in stack
str .= "line: " . entry.line . "`tcalling: " . entry.what "`tfile: " entry.file "`n"
return str
}
generateStack( offset := -1 ) { ;returns the stack as an Array of exception objects - the first array is the function most recently called.
if ( A_IsCompiled )
Throw exception( "Cannot access stack with the exception function" )
stack := []
While ( exData := exception("", -(A_Index-offset)) ).what != -(A_Index-offset)
stack.push( exData )
return stack
}
It's especially useful for cases where you have a seemingly ranom bug appear in a commonly used function like this:
Code: Select all
fn5()
fn4()
fn3()
fn2()
fn1()
fn5() {
fn2()
fn4()
}
fn4() {
fn1()
fn2()
}
fn1() {
fn3()
fn3()
}
fn2() {
fn3()
fn3()
}
fn3() { ;causes bugs randomly
Random, out, 0, 15
if !out {
Msgbox % printStack()
Throw exception( "Some Error occured" )
}
}
printStack() {
stack := generateStack( 2 )
str := "Stack Print, most recent calls first:`n"
for each, entry in stack
str .= "line: " . entry.line . "`tcalling: " . entry.what "`tfile: " entry.file "`n"
return str
}
generateStack( offset := 1 ) {
stack := []
While ( exData := exception("", -(A_Index+offset)) ).what != -(A_Index+offset)
stack.push( exData )
return stack
}
Whats important is the content of the MsgBox - it will tell you a lot about where the error happened.
In this case the error appeared when fn2 called fn3 for the second time.
In this case the error appeared when fn4 called fn1 whose first call to fn3 caused the error.
In this case the error appeared when fn5 called fn2 whose first call to fn3 caused the error.
( This is also the very first place an error could happen. )