4. Error Handling
In programming, there is one absolute rule: a program must never "crash" unexpectedly. All errors that may occur during program execution must be handled, and meaningful error messages must be generated.
If we return to the tax example discussed earlier, what happens if the user enters any value for the number of children? Let’s look at this example:
| C:\>cscript impots1.vbs o xyzt 200000
C:\impots1.vbs(33, 3) Microsoft VBScript runtime error: Incompatible type: 'cint'
|
This is what is known as a wild crash. The "crash" occurred on the statement children=cint(wscript.arguments(1)) because arguments(1) contained the string "xyzt".
Before using a variant whose exact nature is unknown, you must verify its exact subtype. There are several ways to do this:
- test the actual type of the data contained in a variant using the vartype or typename functions
- use a regular expression to verify that the variant’s content matches a specific pattern
- allow the error to occur, then intercept it and handle it
We will examine these different methods.
4.1. Determining the exact type of a data item
Remember that the vartype or varname functions allow you to determine the exact type of a piece of data. This isn’t always very helpful. For example, when we read data typed on the keyboard, the vartype and typename functions will tell us that it is a string of characters, since that is how all data typed on the keyboard is treated. This does not tell us whether this string can, for example, be considered a valid number. We therefore use other functions to access this type of information:
isNumeric(expression) | returns true if expression can be used as a number |
isDate(expression) | returns true if expression can be used as a date |
isEmpty(var) | returns true if the variable var has not been initialized |
isNull(var) | returns true if the variable var contains invalid data |
isArray(var) | returns true if var is an array |
isObject(var) | returns true if var is an object |
The following example prompts the user to enter data via the keyboard until it is recognized as a number:
Program |
' Read input until it is recognized as a number
Option Explicit
Dim end, number
' loop until the entered data is correct
' the loop is controlled by a Boolean variable 'finished', set to false at the start (= not finished)
finished = false
Do While Not finished
' ask for the number
wscript.stdout.write "Enter a number: "
' read the number
number = wscript.stdin.readLine
' the type is necessarily string when reading
wscript.echo "Type of the read data: " & typename(number) & "," & vartype(number)
' we check the actual type of the read data
If isNumeric(number) Then
done = true
Else
wscript.echo "Error, you did not enter a number. Please try again..."
End If
Loop
' confirmation
wscript.echo "Thank you for the number " & number
' and end
wscript.quit 0
|
Results |
| Enter a number: a
Data type read: String,8
Error, you did not enter a number. Please try again...
Enter a number: -12
Type of data read: String,8
Thanks for the number -12
|
|
The isNumeric function does not tell us whether an expression is an integer or not. To get this information, we need to perform additional checks. The following example checks for an integer greater than 0:
Program |
' Read data until it is recognized as an integer greater than 0
Option Explicit
Dim fini, number
' loop until the entered data is correct
' the loop is controlled by a Boolean variable 'finished', set to false at the start (= it's not finished)
finished = false
Do While Not finished
' ask for the number
wscript.stdout.write "Enter an integer >0: "
' read it
number = wscript.stdin.readLine
' check if the read data is a real number
If isNumeric(number) Then
' Is it a positive integer (number equal to its integer part)?
If (number - Int(number)) = 0 And number > 0 Then
result=true
End If
End If
' Possible error message
If Not finished Then wscript.echo "Error: You did not enter an integer greater than 0. Please try again..."
Loop
' confirmation
wscript.echo "Thank you for the integer >0: " & number
' and end
wscript.quit 0
|
Results |
| Enter an integer >0: a
Error, you did not enter an integer >0. Please try again...
Enter an integer >0: -1
Error: You did not enter an integer greater than 0. Please try again...
Enter an integer >0: 10.6
Error: You did not enter an integer greater than 0. Please try again...
Enter an integer >0: 12
Thank you for the integer >0: 12
|
|
Comments:
- int(number) returns the integer part of a number. A number equal to its integer part is an integer.
- It is interesting to note that we had to use the test If (number - int(number)) = 0 And number > 0 because the test If number = int(number) And number > 0 did not yield the expected results. It did not detect positive integers. We leave it to the reader to discover why.
- The test If (number - int(number)) = 0 is not entirely reliable. Let’s look at the following example:
Enter an integer >0: 4.0000000000000000000000001
Thank you for the integer >0: 4.0000000000000000000000001
|
Real numbers are not represented exactly but approximately. And here, the operation number-int(number) returned 0 to the computer's precision.
4.2. Regular expressions
Regular expressions allow us to test the format of a string. This way, we can verify that a string representing a date is in the dd/mm/yy format. To do this, we use a pattern and compare the string to that pattern. Thus, in this example, d, m, and y must be digits. The pattern for a valid date format is then "\d\d/\d\d/\d\d", where the symbol \d denotes a digit. The symbols that can be used in a pattern are as follows (Microsoft documentation):
Character | Description |
\ | Designates the following character as a special character or literal. For example, "n" corresponds to the character "n". "\n" corresponds to a newline character. The sequence "\\" corresponds to "\", while "\(" corresponds to "(". |
^ | Matches the start of the input. |
$ | Matches the end of the input. |
* | Matches the preceding character zero or more times. Thus, "zo*" matches "z" or "zoo". |
+ | Matches the preceding character one or more times. Thus, "zo+" matches "zoo", but not "z". |
? | Matches the preceding character zero or one time. For example, "a?ve?" matches "ve" in "lever". |
. | Matches any single character, except the newline character. |
(pattern) | Searches for the pattern and stores the match. The matching substring can be retrieved from the resulting Matches collection using Item [0]...[n]. To find matches with characters inside parentheses ( ), use "\(" or "\)". |
x|y | Matches either x or y. For example, "z|foot" matches "z" or "foot". "(z|f)oo" matches "zoo" or "foo". |
{n} | n is a non-negative integer. Matches exactly n occurrences of the character. For example, "o{2}" does not match "o" in "Bob," but matches the first two "o"s in "fooooot". |
{n,} | n is a non-negative integer. Matches at least n occurrences of the character. For example, "o{2,}" does not match "o" in "Bob," but matches all "o"s in "fooooot." "o{1,}" is equivalent to "o+" and "o{0,}" is equivalent to "o*." |
{n,m} | m and n are non-negative integers. Matches at least n and at most m occurrences of the character. For example, "o{1,3}" matches the first three "o"s in "foooooot" and "o{0,1}" is equivalent to "o?". |
[xyz] | Character set. Matches any of the specified characters. For example, "[abc]" matches "a" in "plat". |
[^xyz] | Negative character set. Matches any character not listed. For example, "[^abc]" matches "p" in "plat". |
[a-z] | Character range. Matches any character in the specified range. For example, "[a-z]" matches any lowercase alphabetical character between "a" and "z". |
[^m-z] | Negative character range. Matches any character not in the specified range. For example, "[^m-z]" matches any character not between "m" and "z". |
\b | Matches a word boundary, that is, the position between a word and a space. For example, "er\b" matches "er" in "lever," but not "er" in "verb." |
\B | Matches a boundary that does not represent a word. "en*t\B" matches "ent" in "bien entendu". |
\d | Matches a character representing a digit. Equivalent to [0-9]. |
\D | Matches a character that is not a digit. Equivalent to [^0-9]. |
\f | Matches a line break character. |
\n | Matches a newline character. |
\r | Equivalent to a carriage return character. |
\s | Matches any whitespace, including space, tab, page break, etc. Equivalent to "[ \f\n\r\t\v]". |
\S | Matches any non-whitespace character. Equivalent to "[^ \f\n\r\t\v]". |
\t | Matches a tab character. |
\v | Matches a vertical tab character. |
\w | Matches any character representing a word and including an underscore. Equivalent to "[A-Za-z0-9_]". |
\W | Matches any character that does not represent a word. Equivalent to "[^A-Za-z0-9_]". |
\num | Matches num, where num is a positive integer. Refers to stored matches. For example, "(.)\1" matches two consecutive identical characters. |
\n | Matches n, where n is an octal escape value. Octal escape values must consist of 1, 2, or 3 digits. For example, "\11" and "\011" both match a tab character. "\0011" is equivalent to "\001" & "1". Octal escape values must not exceed 256. If they do, only the first two digits are taken into account in the expression. Allows ASCII codes to be used in regular expressions. |
\xn | Corresponds to n, where n is a hexadecimal escape value. Hexadecimal escape values must consist of exactly two digits. For example, "\x41" corresponds to "A". "\x041" is equivalent to "\x04" & "1". Allows the use of ASCII codes in regular expressions. |
An element in a pattern may appear once or multiple times. Let’s look at a few examples involving the \d symbol, which represents a single digit:
pattern | meaning |
\d | a digit |
\d? | 0 or 1 digit |
\d* | 0 or more digits |
\d+ | 1 or more digits |
\d{2} | 2 digits |
\d{3,} | at least 3 digits |
\d{5,7} | between 5 and 7 digits |
Now let’s imagine a model capable of describing the expected format for a string:
target string | pattern |
a date in dd/mm/yy format | \d{2}/\d{2}/\d{2} |
a time in hh:mm:ss format | \d{2}:\d{2}:\d{2} |
an unsigned integer | \d+ |
a sequence of spaces, which may be empty | \s* |
an unsigned integer that may be preceded or followed by spaces | \s*\d+\s* |
an integer that may be signed and preceded or followed by spaces | \s*[+|-]?\s*\d+\s* |
an unsigned real number that may be preceded or followed by spaces | \s*\d+(.\d*)?\s* |
a real number that may be signed and preceded or followed by spaces | \s*[+|]?\s*\d+(.\d*)?\s* |
a string containing the word "just" | \bjuste\b |
| | |
You can specify where to search for the pattern in the string:
pattern | meaning |
^pattern | the pattern starts the string |
pattern$ | the pattern ends the string |
^pattern$ | the pattern starts and ends the string |
pattern | the pattern is searched for anywhere in the string, starting from the beginning. |
search string | pattern |
a string ending with an exclamation point | !$ |
a string ending with a period | \.$ |
a string beginning with the sequence // | ^// |
a string consisting of a single word, optionally preceded or followed by spaces | ^\s*\w+\s*$ |
a string consisting of two words, optionally followed or preceded by spaces | ^\s*\w+\s*\w+\s*$ |
a string containing the word secret | \bsecret\b |
Sub-patterns of a pattern can be "extracted." Thus, not only can we verify that a string matches a particular pattern, but we can also extract from that string the elements corresponding to the sub-patterns of the pattern that have been enclosed in parentheses. For example, if we are parsing a string containing a date in the format dd/mm/yy and want to extract the dd, mm, and yy components of that date, we would use the pattern (\d\d)/(\d\d)/(\d\d).
Let’s look at this example to see how to work with VBScript.
- First, we need to create a RegExp (Regular Expression) object
- Then we set the pattern to be tested
pattern.pattern="(\d\d)/(\d\d)/(\d\d)"
- You may want to ignore case (by default, it is case-sensitive). Here, it doesn’t matter.
pattern.IgnoreCase = true
- You may want to search for the pattern multiple times in the string (by default, this is not done)
A global search only makes sense if the pattern used does not refer to the beginning or end of the string.
- We then search for all matches of the pattern in the string:
set matches = pattern.execute(string)
The execute method of a RegExp object returns a collection of match objects. This object has a value property that is the string element matching the pattern. If you have set pattern.global=true, there may be multiple matches. This is why the result of the execute method is a collection of matches.
- The number of matches is given by matches.count. If this number is 0, it means the pattern was not found anywhere. The value of match #i is given by matches(i).value. If the pattern contains subpatterns in parentheses, then the element of matches(i) corresponding to parenthesis j in the pattern is matches(i).submatches(j).
All of this is shown in the following example:
Program |
' regular expression
' we want to check that a string contains a date in the dd/mm/yy format
Option Explicit
Dim template
' define the pattern
Set pattern = New Regexp
pattern.pattern="\b(\d\d)/(\d\d)/(\d\d)\b" ' a date anywhere in the string
pattern.global = true ' search for the pattern multiple times in the string
' the user provides the string in which the pattern will be searched
Dim string, matches, i
chain=""
' loop as long as string <> "end"
Do While true
' Ask the user to enter text
wscript.stdout.writeLine "Enter text containing dates in dd/mm/yy format and 'end' to stop: "
string = wscript.stdin.readLine
' End if string = "end"
If string="end" Then Exit Do
' Compare the read string to the date template
Set matches = template.execute(string)
' did we find a match
If matches.count <> 0 Then
' we have at least one match
For i = 0 To matches.count - 1
' display match i
wscript.echo "I found the date " & matches(i).value
' retrieve the sub-elements of match i
wscript.echo "The elements of match " & i & " are (" & matches(i).submatches(0) & "," _
& matches(i).submatches(1) & "," & matches(i).submatches(2) & ")"
Next
Else
' no match
wscript.echo "I did not find any dates in the dd/mm/yy format in your text"
End If
Loop
' done
wscript.quit 0
|
Results |
| Type text containing dates in dd/mm/yy format and "end" to stop:
Today is 01/01/01 and tomorrow will be 02/01/02
I found the date 01/01/01
The elements of the date 0 are (01,01,01)
I found the date 02/01/02
The components of the date 1 are (02,01,02)
Type a text containing dates in dd/mm/yy format and press Enter to stop:
a date in the incorrect format: 01/01/2002
I did not find any dates in dd/mm/yy format in your text
Type a text containing dates in dd/mm/yy format and press Enter to stop:
a sequence of dates: 10/10/10, 11/11/11, 12/12/12
I found the date 10/10/10
The components of the date 0 are (10,10,10)
I found the date 11/11/11
The elements of date 1 are (11,11,11)
I found the date 12/12/12
The components of the date 2 are (12,12,12)
Type a text containing dates in dd/mm/yy format and press Enter to stop:
end
|
|
Using regular expressions, the program that checks whether a keyboard input is indeed a positive integer could be written as follows:
Program |
' Read data until it is recognized as a number
Option Explicit
Dim fini, number
' define the pattern for a positive integer (which may be zero)
Dim pattern
Set pattern = New Regexp
pattern.pattern = "^\s*\d+\s*$"
' loop until the entered data is correct
' The loop is controlled by a Boolean variable `fini`, set to false at the start (= not finished)
finished = false
Do While Not finished
' we ask for the number
wscript.stdout.write "Enter an integer >0: "
' read the number
number = wscript.stdin.readLine
' check the format of the read data
Dim matches
Set matches = template.execute(number)
' Has the model been verified?
If matches.count <> 0 Then
' It is an integer, but is it >0?
number = Int(number)
If number > 0 Then
finished=true
End If
End If
' possible error message
If Not finished Then wscript.echo "Error: You did not enter an integer greater than 0. Please try again..."
Loop
' confirmation
wscript.echo "Thank you for the integer >0: " & number
' and end
wscript.quit 0
|
Results |
Enter an integer >0: 10.3
Error, you did not enter an integer >0. Please try again...
Enter an integer >0: abcd
Error: You did not enter an integer >0. Please try again...
Enter an integer >0: -4
Error: You did not enter an integer greater than 0. Please try again...
Enter an integer greater than 0: 0
Error: You did not enter an integer greater than 0. Please try again...
Enter an integer >0: 1
Thank you for the integer >0: 1
|
Finding the regular expression that allows us to verify whether a string matches a certain pattern can sometimes be a real challenge. The following program lets you practice. It asks for a pattern and a string, and then indicates whether or not the string matches the pattern.
Program |
' regular expression
' we want to verify that a string matches a pattern
Option Explicit
' define the pattern
Dim pattern
Set pattern = New Regexp
pattern.global = true ' We will search for the pattern multiple times in the string
' The user provides the string in which to search for the pattern
Dim string, matches, i
Do While True
' the user is prompted to enter a pattern
wscript.stdout.write "Enter the pattern to test and 'end' to stop: "
pattern.pattern = wscript.stdin.readLine
' Done?
If pattern = "end" Then Exit Do
' Ask the user for the strings to compare against the pattern
Do While True
' Ask the user to enter a pattern
wscript.stdout.writeLine "Enter the string to test against the pattern [" & pattern.pattern & "] and 'end' to stop: "
string = wscript.stdin.readLine
' Done?
If string="end" Then Exit Do
' Compare the read string to the date pattern
Set matches = pattern.execute(string)
' Did we find a match?
If matches.count <> 0 Then
' we have at least one match
For i = 0 To matches.count - 1
' display match i
wscript.echo "I found match " & matches(i).value
Next
Else
' no match
wscript.echo "I didn't find a match"
End If
Loop
Loop
' done
wscript.quit 0
|
Results |
| Type the pattern to test and press Enter to stop: ^\s*\d+(\,\d+)*\s*$
Enter the string you want to test using the pattern [^\s*\d+(\,\d+)*\s*$] and press Enter to stop:
18
I found the match [18]
Type the string to test with the pattern [^\s*\d+(\,\d+)*\s*$] and press Enter to stop:
145,678
I didn't find a match
Type the string to test with the pattern [^\s*\d+(\,\d+)*\s*$] and press Enter to stop:
145.678
I found the match [ 145.678 ]
|
|
4.3. Handling runtime errors
Another method for handling runtime errors is to let them occur, be notified of them, and then handle them. Normally, when an error occurs during execution, WSH displays an error message and the program stops. Two statements allow us to change this behavior:
This statement tells the system (WSH) that we will handle errors ourselves. After this statement, any errors are simply ignored by the system.
This statement returns us to normal error handling.
When the on error resume next statement is active, we must handle any errors that may occur ourselves. The Err object helps us do this. This object has various properties and methods, of which we will focus on the following two:
- number: an integer representing the number of the last error that occurred. 0 means "no error"
- description: the error message the system would have displayed if we hadn’t issued the on error resume next statement
Let’s look at the following example:
Program | Results |
' unhandled error
Option Explicit
Dim number
number = cdbl("abcd")
wscript.echo "number=" & number
|
| C:\ err5.vbs(6, 1) Microsoft VBScript runtime error: Incompatible type: 'cdbl'
|
|
Now let's handle the error:
Program | Results |
' error handled
Option Explicit
Dim number
' we handle errors ourselves
On Error Resume Next
number = CDBL("abcd")
' Was there an error?
If Err.Number <> 0 Then
wscript.echo "Error [" & err.description & "] occurred"
On Error GoTo 0
wscript.quit 1
End If
' No error - return to normal operation
On Error GoTo 0
wscript.echo "number=" & number
wscript.quit 0
|
| The [Incompatible Type] error occurred
|
|
Let's rewrite the program for entering an integer >0 using this new method:
Program |
' Read data until it is recognized as a number
Option Explicit
Dim finished, number
' loop until the entered data is correct
' the loop is controlled by a Boolean variable 'finished', set to false at the start (= not finished)
finished = false
Do While Not finished
' ask for the number
wscript.stdout.write "Enter an integer >0: "
' read the number
number = wscript.stdin.readLine
' check the format of the read data
On Error Resume Next
number = CDBL(number)
If err.number = 0 Then
' no error, it's a number
' return to normal error handling mode
On Error GoTo 0
' Is it an integer greater than 0?
If (number - Int(number)) = 0 And number > 0 Then
finished=true
End If
End If
' return to normal error handling mode
On Error GoTo 0
' possible error message
If Not finished Then wscript.echo "Error: You did not enter an integer greater than 0. Please try again..."
Loop
' confirmation
wscript.echo "Thank you for the integer >0: " & number
' and end
wscript.quit 0
|
Results |
Enter an integer >0: 4.5
Error, you did not enter an integer >0. Please try again...
Enter an integer >0: 4.5
Error: You did not enter an integer greater than 0. Please try again...
Enter an integer >0: abcd
Error: You did not enter an integer greater than 0. Please try again...
Enter an integer greater than 0: -4
Error: You did not enter an integer greater than 0. Please try again...
Enter an integer >0: 1
Thank you for the integer >0: 1
|
Comments:
- This method is sometimes the only one that can be used. In that case, do not forget to return to normal error handling mode as soon as the sequence of instructions likely to generate the error is complete.
4.4. Application to the tax calculation program
We’ll revisit the tax calculation program we wrote earlier to, this time, verify the validity of the arguments passed to the program:
Program |
' Calculating a taxpayer's tax
' the program must be called with three parameters: married children salary
' married: character O if married, N if unmarried
' children: number of children
' salary: annual salary without cents
' no data validation is performed, but we
' we do check that there are exactly three
' mandatory variable declaration
Option Explicit
Dim syntax
syntax = _
"Syntax: pg married children salary" & vbCRLF & _
"married: character O if married, N if unmarried" & vbCRLF & _
"children: number of children (integer >=0)" & vbCRLF & _
"salary: annual salary without cents (integer >=0)"
' Check that there are 3 arguments
Dim nbArguments
nbArguments = wscript.arguments.count
If nbArguments<>3 Then
' error message
wscript.echo syntax & vbCRLF & vbCRLF & "error: incorrect number of arguments"
' exit with error code 1
wscript.quit 1
End If
' retrieve the arguments and check their validity
' An argument is passed to the program without spaces before or after
' we will use regular expressions to verify the validity of the data
Dim pattern, matches
Set pattern = new regexp
' the marital status must consist of the characters oOnN
pattern.pattern="^[oOnN]$"
Set matches = template.execute(wscript.arguments(0))
If matches.count=0 Then
' error
wscript.echo syntax & vbCRLF & vbCRLF & "error: incorrect marital status argument"
' exit
wscript.quit 2
End If
' retrieve the value
Dim marie
If lcase(wscript.arguments(0)) = "o" Then
marie = true
Else
married=false
End If
' children must be an integer >=0
pattern.pattern="^\d{1,2}$"
Set matches = pattern.execute(wscript.arguments(1))
If matches.count=0 Then
' error
wscript.echo syntax & vbCRLF & vbCRLF & "error: incorrect children argument"
' exit
wscript.quit 3
End If
' retrieve the value
Dim children
children = Int(WScript.Arguments(1))
' salary must be an integer >=0
pattern.pattern="^\d{1,9}$"
Set matches = pattern.execute(wscript.arguments(2))
If matches.count=0 Then
' error
wscript.echo syntax & vbCRLF & vbCRLF & "error: incorrect salary argument"
' exit
wscript.quit 4
End If
' retrieve the value
Dim salary
salary = clng(wscript.arguments(2))
' define the data needed to calculate the tax in 3 tables
Dim limits, coeffn, coeffr
limits = array(12620, 13190, 15640, 24740, 31810, 39970, 48360, _
55790,92970,127860,151250,172040,195000,0)
coeffr=array(0,0.05,0.1,0.15,0.2,0.25,0.3,0.35,0.4,0.45, _
0.5,0.55,0.6,0.65)
coeffn=array(0,631,1290.5,2072.5,3309.5,4900,6898.5,9316.5, _
12106,16754.5,23147.5,30710,39312,49062)
' we calculate the number of parts
Dim nbParts
If marie=true Then
nbParts=(children/2)+2
Else
nbParts = (children / 2) + 1
End If
If children >= 3 Then nbParts = nbParts + 0.5
' Calculate the family quotient and taxable income
Dim income, qf
income = 0.72 * salary
qf = income / nbParts
' Calculate the tax
Dim i, tax
i = 0
Do While i < ubound(limits) And qf > limits(i)
i = i + 1
Loop
tax = int(income * coeffr(i) - nbParts * coeffn(i))
' display the result
wscript.echo "tax=" & tax
' exit without error
wscript.quit 0
|
Results |
C:\>cscript impots2.vbs
Syntax: pg married children salary
married: character O if married, N if unmarried
children: number of children (integer >=0)
salary: annual salary without cents (integer >=0)
error: incorrect number of arguments
C:\>cscript impots2.vbs a b c
Syntax: pg married children salary
married: character O if married, N if unmarried
children: number of children (integer >=0)
salary: annual salary without cents (integer >=0)
Error: Invalid argument "marie"
C:\>cscript impots2.vbs o b c
Syntax: pg married children salary
married: character O if married, N if unmarried
children: number of children (integer >=0)
salary: annual salary without cents (integer >=0)
error: incorrect children argument
C:\>cscript impots2.vbs o 2 c
Syntax: pg married children salary
married: character O if married, N if unmarried
children: number of children (integer >=0)
salary: annual salary without cents (integer >=0)
error: incorrect salary argument
C:\>cscript impots2.vbs o 2 200000
tax=22504
|