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:
This is what is known as a wild crash. The instruction enfants=cint(wscript.arguments(1)) caused a "crash" 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 and varname functions let you 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, since all data typed on the keyboard is treated as such. 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 data until it is recognized as a number
Option Explicit
Dim fini, nombre
' loop until the data entered is correct
' the loop is controlled by a finite boolean, set to false at the start (= it's not finite)
fini=false
Do While Not fini
' we ask for the number
wscript.stdout.write "Tapez un nombre : "
' we read it
nombre=wscript.stdin.readLine
' the type is necessarily string when read
wscript.echo "Type de la donnée lue : " & typename(nombre) & "," & vartype(nombre)
' test the actual type of data read
If isNumeric(nombre) Then
fini=true
Else
wscript.echo "Erreur, vous n'avez pas tapé un nombre. Recommencez svp..."
End If
Loop
' confirmation
wscript.echo "Merci pour le nombre " & nombre
' and end
wscript.quit 0
Results
The isNumeric function does not tell us whether an expression is an integer or not. To obtain this information, additional tests are required. The following example requests an integer greater than 0:
Program
' read data until it is recognized as an integer >0
Option Explicit
Dim fini, nombre
' loop until the data entered is correct
' the loop is controlled by a finite boolean, set to false at the start (= it's not finite)
fini=false
Do While Not fini
' we ask for the number
wscript.stdout.write "Tapez un nombre entier >0: "
' we read it
nombre=wscript.stdin.readLine
' test the actual type of data read
If isNumeric(nombre) Then
' is it a positive integer (number equal to its integer part)?
If (nombre-int(nombre))=0 And nombre>0 Then
fini=true
End If
End If
' possible error msg
If Not fini Then wscript.echo "Erreur, vous n'avez pas tapé un nombre entier >0. Recommencez svp..."
Loop
' confirmation
wscript.echo "Merci pour le nombre entier >0 : " & nombre
' and end
wscript.quit 0
Results
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:
|
Real numbers are not represented exactly but approximately. And here, the operation number-int(number) returned 0 to the precision of the computer.
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}" matches "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 do this with VBScript.
- First, we need to create a RegExp (Regular Expression) object
- Then we set the pattern to be tested
- You may want to ignore case (by default, it is case-sensitive). Here, it doesn’t matter.
- 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:
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 dd/mm/yy format
Option Explicit
Dim modele
' we define the
Set modele=new regexp
modele.pattern="\b(\d\d)/(\d\d)/(\d\d)\b" ' a date anywhere in the chain
modele.global=true ' we will search for the model several times in the chain
' the user specifies the string in which to search for the model
Dim chaine, correspondances, i
chaine=""
' loop as long as string<>"end"
Do While true
' the user is asked to type a text
wscript.stdout.writeLine "Tapez un texte contenant des dates au format jj/mm/aa et fin pour arrêter : "
chaine=wscript.stdin.readLine
' fini si chaine=fin
If chaine="fin" Then Exit Do
' the string read is compared with the date template
Set correspondances=modele.execute(chaine)
' was a match found
If correspondances.count<>0 Then
' we have at least one match
For i=0 To correspondances.count-1
' the i correspondence is displayed
wscript.echo "J'ai trouvé la date " & correspondances(i).value
' we retrieve the sub-elements of correspondence i
wscript.echo "Les éléments de la date " & i & " sont (" & correspondances(i).submatches(0) & "," _
& correspondances(i).submatches(1) & "," & correspondances(i).submatches(2) & ")"
Next
Else
' no correspondence
wscript.echo "Je n'ai pas trouvé de date au format jj/mm/aa dans votre texte"
End If
Loop
' finish
wscript.quit 0
Results
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, nombre
' we define the model of a positive integer (which may be zero)
Dim modele
Set modele=new regexp
modele.pattern="^\s*\d+\s*$"
' loop until the data entered is correct
' the loop is controlled by a finite boolean, set to false at the start (= it's not finite)
fini=false
Do While Not fini
' we ask for the number
wscript.stdout.write "Tapez un nombre entier >0: "
' we read it
nombre=wscript.stdin.readLine
' test the format of the data read
Dim correspondances
Set correspondances=modele.execute(nombre)
' has the model been checked?
If correspondances.count<>0 Then
' it's an integer, but is it >0?
nombre=cint(nombre)
If nombre>0 Then
fini=true
End If
End If
' possible error msg
If Not fini Then wscript.echo "Erreur, vous n'avez pas tapé un nombre entier >0. Recommencez svp..."
Loop
' confirmation
wscript.echo "Merci pour le nombre entier >0 : " & nombre
' and end
wscript.quit 0
Results
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 check that a chain corresponds to a model
Option Explicit
' we define the
Dim modele
Set modele=new regexp
modele.global=true ' we will search for the model several times in the chain
' the user specifies the string in which to search for the model
Dim chaine, correspondances, i
Do While true
' the user is asked to type in a template
wscript.stdout.write "Tapez le modèle à tester et fin pour arrêter : "
modele.pattern=wscript.stdin.readLine
' finished?
If modele.pattern="fin" Then Exit Do
' the user is asked for the strings to be compared with the model
Do While true
' the user is asked to type in a template
wscript.stdout.writeLine "Tapez la chaîne à tester avec le modèle [" & modele.pattern & "] et fin pour arrêter : "
chaine=wscript.stdin.readLine
' finished?
If chaine="fin" Then Exit Do
' the string read is compared with the date template
Set correspondances=modele.execute(chaine)
' was a match found
If correspondances.count<>0 Then
' we have at least one match
For i=0 To correspondances.count-1
' the i correspondence is displayed
wscript.echo "J'ai trouvé la correspondance " & correspondances(i).value
Next
Else
' no correspondence
wscript.echo "Je n'ai pas trouvé de correspondance"
End If
Loop
Loop
' finish
wscript.quit 0
Results
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 at runtime, 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
' unhandled error
Option Explicit
Dim nombre
nombre=cdbl("abcd")
wscript.echo "nombre=" & nombre
Results
Now let's handle the error:
Program
' managed error
Option Explicit
Dim nombre
' we manage mistakes ourselves
On Error Resume Next
nombre=cdbl("abcd")
' was there a mistake?
If Err.number<>0 Then
wscript.echo "L'erreur [" & err.description & "] s'est produite"
On Error GoTo 0
wscript.quit 1
End If
' no error - returns to normal operation
On Error GoTo 0
wscript.echo "nombre=" & nombre
wscript.quit 0
Results
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 fini, nombre
' loop until the data entered is correct
' the loop is controlled by a finite boolean, set to false at the start (= it's not finite)
fini=false
Do While Not fini
' we ask for the number
wscript.stdout.write "Tapez un nombre entier >0: "
' we read it
nombre=wscript.stdin.readLine
' test the format of the data read
On Error Resume Next
nombre=cdbl(nombre)
If err.number=0 Then
' no error it's a number
' returns to normal error handling mode
On Error GoTo 0
' is it an integer >0
If (nombre-int(nombre))=0 And nombre>0 Then
fini=true
End If
End If
' returns to normal error handling mode
On Error GoTo 0
' possible error msg
If Not fini Then wscript.echo "Erreur, vous n'avez pas tapé un nombre entier >0. Recommencez svp..."
Loop
' confirmation
wscript.echo "Merci pour le nombre entier >0 : " & nombre
' and end
wscript.quit 0
Results
Tapez un nombre entier >0: 4.5
Erreur, vous n'avez pas tapé un nombre entier >0. Recommencez svp...
Tapez un nombre entier >0: 4,5
Erreur, vous n'avez pas tapé un nombre entier >0. Recommencez svp...
Tapez un nombre entier >0: abcd
Erreur, vous n'avez pas tapé un nombre entier >0. Recommencez svp...
Tapez un nombre entier >0: -4
Erreur, vous n'avez pas tapé un nombre entier >0. Recommencez svp...
Tapez un nombre entier >0: 1
Merci pour le nombre entier >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 liability
' the program must be called with three parameters: married children salary
' married: character Y if married, N if unmarried
' children: number of children
' salary: annual salary without cents
' no verification of data validity is performed, but we do
' check that there are three of them
' mandatory variable declaration
Option Explicit
Dim syntaxe
syntaxe= _
"Syntaxe : pg marié enfants salaire" & vbCRLF & _
"marié : caractère O si marié, N si non marié" & vbCRLF & _
"enfants : nombre d'enfants (entier >=0)" & vbCRLF & _
"salaire : salaire annuel sans les centimes (entier >=0)"
' we check that there are 3 arguments
Dim nbArguments
nbArguments=wscript.arguments.count
If nbArguments<>3 Then
' error msg
wscript.echo syntaxe & vbCRLF & vbCRLF & "erreur : nombre d'arguments incorrect"
' stop with error code 1
wscript.quit 1
End If
' retrieve arguments and check their validity
' an argument is passed to the program without spaces in front and behind it
' use regular expressions to check data validity
Dim modele, correspondances
Set modele=new regexp
' marital status must be among the characters oOnN
modele.pattern="^[oOnN]$"
Set correspondances=modele.execute(wscript.arguments(0))
If correspondances.count=0 Then
' error
wscript.echo syntaxe & vbCRLF & vbCRLF & "erreur : argument marie incorrect"
' we leave
wscript.quit 2
End If
' the value
Dim marie
If lcase(wscript.arguments(0)) = "o"Then
marie=true
Else
marie=false
End If
' children must be an integer >=0
modele.pattern="^\d{1,2}$"
Set correspondances=modele.execute(wscript.arguments(1))
If correspondances.count=0 Then
' error
wscript.echo syntaxe & vbCRLF & vbCRLF & "erreur : argument enfants incorrect"
' we leave
wscript.quit 3
End If
' the value
Dim enfants
enfants=cint(wscript.arguments(1))
' salary must be an integer >=0
modele.pattern="^\d{1,9}$"
Set correspondances=modele.execute(wscript.arguments(2))
If correspondances.count=0 Then
' error
wscript.echo syntaxe & vbCRLF & vbCRLF & "erreur : argument salaire incorrect"
' we leave
wscript.quit 4
End If
' the value
Dim salaire
salaire=clng(wscript.arguments(2))
' we define the data needed to calculate the tax in 3 tables
Dim limites, coeffn, coeffr
limites=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)
' the number of shares is calculated
Dim nbParts
If marie=true Then
nbParts=(enfants/2)+2
Else
nbParts=(enfants/2)+1
End If
If enfants>=3 Then nbParts=nbParts+0.5
' calculate the family quota and taxable income
Dim revenu, qf
revenu=0.72*salaire
qf=revenu/nbParts
' tax calculation
Dim i, impot
i=0
Do While i<ubound(limites) And qf>limites(i)
i=i+1
Loop
impot=int(revenu*coeffr(i)-nbParts*coeffn(i))
' the result is displayed
wscript.echo "impôt=" & impot
' leave without error
wscript.quit 0
Results
C:\>cscript impots2.vbs
Syntaxe : pg marié enfants salaire
marié : caractère O si marié, N si non marié
enfants : nombre d'enfants (entier >=0)
salaire : salaire annuel sans les centimes (entier >=0)
erreur : nombre d'arguments incorrect
C:\>cscript impots2.vbs a b c
Syntaxe : pg marié enfants salaire
marié : caractère O si marié, N si non marié
enfants : nombre d'enfants (entier >=0)
salaire : salaire annuel sans les centimes (entier >=0)
erreur : argument marie incorrect
C:\>cscript impots2.vbs o b c
Syntaxe : pg marié enfants salaire
marié : caractère O si marié, N si non marié
enfants : nombre d'enfants (entier >=0)
salaire : salaire annuel sans les centimes (entier >=0)
erreur : argument enfants incorrect
C:\>cscript impots2.vbs o 2 c
Syntaxe : pg marié enfants salaire
marié : caractère O si marié, N si non marié
enfants : nombre d'enfants (entier >=0)
salaire : salaire annuel sans les centimes (entier >=0)
erreur : argument salaire incorrect
C:\>cscript impots2.vbs o 2 200000
impôt=22504