This is a simple and easy to follow Error System for AutoHotkey.
Why should I use this? I already use <x>.
This is to help debug where your code went wrong and how to fix it. It works pretty much exactly how Python does their Error System.
Why not just throw an Exception?
Throwing an exception just to say there was a problem may work; however, it does not provide the easiest way to help debug your scripts.
Well, how do I use this?
Simple, wherever you want to throw an error for a problem that may have occurred, you do the following: throw new Exception("Explaination", ExtraInfo).
Replace Exception for the error type.
Replace "Explaination" for some help as to why the error was called, you can add "{extra}" within the string to add ExtraInfo into the string to help..
Replace ExtraInfo for any extra information, such as the variable that caused the problem (Optional).
Great, how to I "install" this?
Simply use #Include <ErrorLib> where "ErrorLib" is the name of the file you save this library to. It is recommended to include the code at the top.
Is there anything I need to worry about?
Besides the OnError call being overridden for this to work, nope.
Example:
Code: Select all
#include <ErrorLib>
main(args) {
if (args.Count() < 1) {
throw new Exception("expected 1 (one) or more arguments for this script, got {extra}", args.Count())
}
for index, value in args {
if (value != "/close") {
throw new ValueError("expected '/close' as argument", value)
}
}
}
; Same as `if __name__ == '__main__':` in Python.
; Basically just checks if this file (this example) is being called directly or being included in another script.
if (A_LineFile == A_ScriptFullPath) {
try {
main(A_Args)
} catch error_object {
switch (error_object.name) {
case "ValueError":
if (error_object.extra == "-hello_world") {
print("Nothing to see here, just a friendly greeting :)")
}
; If we didn't expect the error, just print it.
default:
print(format_error_obj(error_object))
}
}
}
; Nothing else to do, so let's close the script.
ExitApp, 0
Spoiler
Code: Select all
/*
****************************************************************************************
* Error.ahk:
* Just a few functions and error system useful for debugging.
* All errors return similar to Python's error system:
* Traceback (most recent call last):
* File "C:\MyScripts\main.ahk", line 99, in <module>
* custom_assert(10 + 10 = 12)
* Exception: assertion failed!
****************************************************************************************
*/
Type(V, Assert = False) {
Static nMatchObj := NumGet(&(m, RegExMatch(Null, "O)", m)))
Static nBoundFunc := NumGet(&(F := Func("Func").Bind()))
Static nFileObj := NumGet(&(F := FileOpen("*", "w")))
Local T := Null
If (IsObject(V)) {
T := (V.__Class != Null ? "Class"
: V.SetCapacity(0) = (V.MaxIndex() - V.MinIndex() + 1) ? "Array"
: ObjGetCapacity(V) != Null ? "Object"
: IsFunc(V) ? "Func"
: IsLabel(V) ? "Label"
: ComObjType(V) != Null ? "ComObject"
: NumGet(&V) == nBoundFunc ? "BoundFunc"
: NumGet(&V) == nMatchObj ? "RegExMatchObject"
: NumGet(&V) == nFileObj ? "FileObject"
: . "Property")
} Else {
If (V == Null) {
T := "Undefined"
} Else {
T := ((ObjGetCapacity([V], 1) != Null) ? ("String") : (InStr(V, ".") ? "Float" : "Integer"))
}
}
If (Assert != False) {
If (InStr(Assert, "|")) {
For I, Val in StrSplit(Assert, "|") {
If (InStr(T, Val)) {
Return True
}
}
Return False
} Else If (Type(Assert) = "Array") {
For I, Val in Assert {
If (InStr(T, Val)) {
Return True
}
}
Return False
}
}
Return T
}
ParseArray(Obj, Depth = 10, IndentLevel = " ", LimitCount = -1, FormatEscapes = True) {
; Read through an array and list the variables values and names.
If (type(Obj) != "object") {
Return Obj
}
For k, v in Obj {
If (type(v, "object") && (Depth > 1)) {
If (type(v.name) = "func") {
middlepart .= "func(""" . v.name . """)`r`n"
If (type(k) = "integer") {
ToReturn .= IndentLevel . "- " . middlepart
} Else {
If (FormatEscapes == True) {
k := StrReplace(k, "`t", "\t")
k := StrReplace(k, "`r", "\r")
k := StrReplace(k, "`n", "\n")
}
ToReturn .= IndentLevel . k . ": " . middlepart
}
} Else {
middlepart := "`r`n" . ParseArray(v, Depth - 1, IndentLevel . " ", LimitCount, FormatEscapes)
If (type(k) = "integer") {
ToReturn .= IndentLevel . "- " . middlepart
} Else {
If (FormatEscapes == True) {
k := StrReplace(k, "`t", "\t")
k := StrReplace(k, "`r", "\r")
k := StrReplace(k, "`n", "\n")
}
ToReturn .= IndentLevel . k . ": " . middlepart
}
}
} Else {
If (FormatEscapes == True) {
v := StrReplace(v, "`t", "\t")
v := StrReplace(v, "`r", "\r")
v := StrReplace(v, "`n", "\n")
}
If (k ~= "^[0-9]$") {
v := ((LimitCount != -1) ? ((StrLen(v) > LimitCount) ? (SubStr(v, 1, LimitCount) . " [...]") : (v)) : (v))
ToReturn .= IndentLevel . "- " . v . "`r`n"
} Else {
If (FormatEscapes == True) {
k := StrReplace(k, "`t", "\t")
k := StrReplace(k, "`r", "\r")
k := StrReplace(k, "`n", "\n")
}
v := ((LimitCount != -1) ? ((StrLen(v) > LimitCount) ? (SubStr(v, 1, LimitCount) . " [...]") : (v)) : (v))
ToReturn .= IndentLevel . k . ": " . v . "`r`n"
}
}
}
; Since I'm too lazy to redo this function to not have this happen:
; vars:
; -
; name: "Jay"
; When this should happen:
; vars:
; - name: "Jay"
; I just use this:
ToReturn := RegExReplace(ToReturn, "\- \R {2,}", "- ")
Return RTrim(ToReturn)
}
print(msg, args*) {
result := ""
last_arg := args[args.count()]
file := "*"
end := "`n"
if (type(last_arg) = "object") {
file := ((last_arg.file != Null) ? (last_arg.file) : (file))
end := ((last_arg.end != Null) ? (last_arg.end) : (end))
}
if (type(msg) = "object") {
result := ParseArray(msg)
} else {
msg := StrReplace(msg, "{", "{{}")
msg := StrReplace(msg, "}", "{}}")
for index in args {
; print(" name: %1`n fav_color: %2:x%", "Bob", 123456)
msg := RegExReplace(msg, "\`%([0-9]{1,3})(?:\:(.+)?\`%)?", "{$1:$2}")
}
result := Format(msg, args*)
}
switch (file) {
case "OutputDebug":
OutputDebug, % result . end
case "Return":
return (result . end)
default:
FileAppend, % result . end, % file
}
return Null
}
; If an error was thrown and not caught by an upper layer in the script, this function is called.
__control_error(Error) {
; We add this static call here to set the error function to be this
static init := OnError("__control_error")
print(format_error_obj(error))
ExitApp, 1
return True
}
; Format error objects created by the Exception classes and its heirs.
format_error_obj(error_object) {
if (error_object.HasKey("stack_trace")) {
; Add some info for what this is in this error message.
; Basically just a tip for people reading the error.
str := "Traceback (most recent call last):`n"
; For each deep callback in the error, we give where it was called from.
; <main> is the master scope. Basically the start of where the function was called.
loop, % error_object.stack_trace.count() - 1 {
i := error_object.stack_trace.count() - (A_Index - 1)
obj := error_object.stack_trace[i]
str .= Format(" File ""{1:}"", line {2:}, in {3:}`n", obj.file, obj.line, obj.where)
if (!A_IsCompiled) {
FileReadLine, line, % obj.file, % obj.line
str .= " " . Trim(line) . "`n"
}
}
; Then we give what the error was and why it went wrong.
str .= error_object.name . ": " . StrReplace(error_object.message, "{extra}", error_object.extra)
} else {
spec := ((error_object.Extra != Null) ? ("`n Specifically: {4:}"))
str := Format("{1:} ({2:}) : ==> {3:}" . spec
, error_object.File, error_object.Line, error_object.Message, error_object.Extra)
}
return str
}
class Exception {
__new(msg = "", extra = "", skip_frames = -1) {
/*
; To get the top-most called function/method we do this... thing.
; Skipped frames means how far back we start from.
Class MyClass { ; -2
my_func() { ; -1
my_call() ; 0 (default)
}
}
*/
while ((stk := Exception(Null, 0 - A_Index - 1)).What != (0 - A_Index - 1)) {
if (skip_frames-- <= 0) {
break
}
}
; The built-in error dialog requires that these be set raw.
ObjRawSet(this, "Message", msg)
ObjRawSet(this, "Extra", ((type(extra) = "object") ? (ParseArray(extra)) : (extra)))
ObjRawSet(this, "File", stk.File)
ObjRawSet(this, "Line", stk.Line)
ObjRawSet(this, "Name", this.__class)
; Since the built-in error dialog doesn't use this variable, we're free to set it normally.
this.stack_trace := Exception.stack_trace(skip_frames)
return this
}
stack_trace(n := -1) {
trace := []
next := Exception(Null, n)
while ((stk := next).What != n) {
next := Exception("", --n)
ctx := (next.What < 0 ? "<module>" : next.What)
trace.push({file: stk.File, line: stk.Line, where: ctx})
}
return trace
}
}
; Value's type is not expected (expected string, got integer).
class TypeError extends Exception {
__new(p*) {
p[3] := (p[3] ? (p[3] - 1) : (-2))
base.__new(p*)
if (ObjHasKey(p, 2) && (type(p[2]) != "object")) {
this.Extra .= " (" . type(p[2]) . ")"
}
ObjRawSet(this, "Name", this.__class)
}
}
; Getting/setting a member within a class that doesn't exist.
; Members are keys within a class that are protected or private. Usually beginning with an underscore "_", or double underscore "__".
; This is mostly used inside __get or __set.
class MemberError extends Exception {
}
; Getting/setting a property within a class that doesn't exist.
; Properties are named members of classes.
; This is mostly used inside __get or __set.
class PropertyError extends MemberError {
}
; For when a method is attempted to be called that does not exist.
; This is mostly used inside __call.
class MethodError extends MemberError {
}
; For when the value of a variable/return value/parameter is not expected (expected between 1 and 10, got 22).
class ValueError extends Exception {
}
; For when the number of an array or key of an object does not exist.
class IndexError extends ValueError {
}