Prepare!

Lee Harvey's Zombie Hit Parade

Try..Catch..Finally in ... VBScript? Sure!

, , , , ,

I've been working a lot with VBScript and Windows Scripting Host (WSH) lately. Unfortunately, as many of you VBScript writers know, the error-handling capabilities of VBScript leave a lot to be desired. Luckily, a simple design coding pattern from modern programming languages has revealed a rather unique technique in VBScript that resembles...well, a Try..Catch..Finally block! Yep, you heard correctly. With a liberal dose of multi-line colons ( : ), your VBScript code can attain near perfect Try..Catch..Finally functionality. Note: Guaranteed error-handling, and guaranteed finally block execution in a pseudo-subroutine fashion -- all without requiring an "On Error" statement!

Too good to be true? Here's the catch (no pun intended): You leverage the object lifespan provided by VBScript objects. VBScript supports OOP? Yes sir! Grab a copy of your favorite VBScript documentation, and do a search for "Class".

Basically, here's some pseudo-code that illustrates what we all wish VBScript supported...(yeah, I know VB.NET supports this, bah!)

Sub Func1
   Try
      DoLog "Starting"
      Dim i : i = 65535 ^ 65535 
      MsgBox "Should not see this"
   Catch e
      Select Case e.Number
         Case 6 DoLog "Overflow handled!"
         Case Else DoLog "Unhandled error " & e.Number & " occurred."
      End Select
   Finally
      DoLog "Exiting"
   End Try
End Sub
Call Func1

And here's a reality check...

Class CFunc1
   Private Sub Class_Initialize
      DoLog "Starting"
      Dim i : i = 65535 ^ 65535 
      MsgBox "Should not see this"
   End Sub : Private Sub CatchErr : If Err.Number = 0 Then Exit Sub
      Select Case Err.Number
         Case 6 DoLog "Overflow handled!" 
         Case Else DoLog "Unhandled error " & Err.Number & " occurred."
      End Select
   Err.Clear : End Sub : Private Sub Class_Terminate : CatchErr
      DoLog "Exiting" 
   End Sub 
End Class
Dim Func1 : Set Func1 = New CFunc1 : Set Func1 = Nothing

See a resemblance between the two examples? Sure. And guess what? While the first example doesn't work in VBScript, the second example runs just fine in VBScript 5.6 -- once the DoLog subrountine is defined smile

So explain, how does it work? Simple, really. You are defining a Class rather than a Sub-routine. Instantiated Classes (called objects) have a predefined lifespan: a constructor is guaranteed to run once the object is created; and a destructor is guaranteed to run once the object is destroyed. You simply leverage these innate object abilities to handle errors gracefully -- all without using "On Error" statements!

I hope you find this exercise fun, if not useful. I've been rewriting some larger WSH VBScripts using this new technique, and so far so good. I've thrown some various crash scenerios (from the past) at them, and not only did they manage to gracefully handle the errors, but the scripts completely finished without dying mid-way through them -- like they used to.

Enjoy.

Proxy Automatic Config (PAC) File TipsGrisoft AVG Free and encrypted All Users\Application Data folder = bad

Comments

LenChaney Thursday, August 23, 2007 1:57:05 PM

Very neat. I used the idea of the Class_Terminate being always called before the page goes out of scope for an ASP page. This way, I can always catch the errors.

Can you give me an example of how you would use this in your WSH script. I understand how it works but I was wondering how you would embed it in your script. I'm also looking to use it in ASP. Most of my development is in Classic ASP. We haven't been Dotted yet.

Thanks,

Len

Lee HarveyLee_Harvey Thursday, August 23, 2007 7:37:37 PM

Here's a quick example that retrieves Symantec's ThreatCon Level, and outputs it to the console.
Option Explicit

Dim exitcode : exitcode = 0

Sub HandleError(ByVal c)
   exitcode = exitcode Or Err.Number
   If Err.Number = 0 Then Exit Sub
   Select Case Err.Number
      Case Else
         WScript.StdErr.WriteLine "Unhandled " & c & " error " & Err.Number & ": " & Err.Description
   End Select
   Err.Clear
End Sub

Class CWeb
   Private xml

   Private Sub Class_Initialize()
      Set xml = CreateObject("Msxml2.XMLHTTP")
   End Sub

   Private Sub Class_Terminate()
      Set xml = Nothing
      HandleError "CWeb"
   End Sub

   Public Function GetPage(ByVal url)
      xml.Open "GET", url, False
      xml.Send
      GetPage = xml.responseText
   End Function
End Class

Class CThreatCon
   Private regEx
   Private match
   Private matches

   Public level
   Public msg

   Private Sub Class_Initialize()
      Set regEx = New RegExp
   End Sub

   Private Sub Class_Terminate()
      Set regEx = Nothing
      Set matches = Nothing
      HandleError "CThreatCon"
   End Sub

   Public Sub Parse(ByVal s)
      regEx.IgnoreCase = False
      regEx.Global = False
      regEx.Pattern = "<img src=""/img/threatcon/threatcon_level(%5B0-9%5D)\.gif"" border=0></p>\W+<p>([^<]+)<"
      Set matches = regEx.Execute(s)
      For Each match In matches
         level = match.SubMatches(0)
         msg = match.SubMatches(1)
      Next
   End Sub
End Class

Class CAllSystems
   Private web
   Private threatCon

   Private Sub Class_Initialize()
      Err.Clear
      Set web = New CWeb
      Set threatCon = New CThreatCon
   End Sub

   Private Sub Class_Terminate()
      Set web = Nothing
      Set threatCon = Nothing
      HandleError "CAllSystems"
   End Sub

   Public Sub Go()
      threatCon.Parse web.GetPage("http://www.symantec.com/avcenter/threatcon/")
      WScript.StdOut.WriteLine "Level=" & threatCon.level & ".  " & threatCon.msg
   End Sub
End Class

Dim AllSystems : Set AllSystems = New CAllSystems
AllSystems.Go
Set AllSystems = Nothing

WScript.Quit exitcode
Note: In this case, I used a global error-handling subroutine for the classes.

Eddie AdamsEddieAdams Sunday, March 22, 2009 11:05:24 PM

Hi,

It's been a while and I hope you're still active here.

First off, thanks for the tips.

My question: Is there a way to make a VBscript with classes silent? For instance, if I am now handling my error output to a log file is there a way I can switch off the M$ generated message to the console?

Eddie.

Lee HarveyLee_Harvey Monday, March 23, 2009 12:07:43 AM

Absolutely.

If you are creating, opening, and writing to the log file inside your VBScript using Scripting.FileSystemObject -- rather than using standard console redirection (> or >>) -- then try using the //B command-line option, which runs the script in Batch mode, and suppresses all StdErr and StdOut output during the lifetime of your script. For example:

   cscript.exe //e:vbscript //nologo //b myscript.vbs

Otherwise, you can set Interactive mode ON/OFF inside your VBScript using the following line:

   WScript.Interactive = False ' or True

...which dynamically controls if/when your script can produce output. Very handy, IMO.

Obviously, you can also use "On Error Resume Next" to suppress error output, then use "On Error Goto 0" to re-enable error reporting. But without explicitly handling suppressed errors, your script may have unexpected results or behaviors.

Another technique is to use a simplified parent script to invoke the main child script, and capture the StdErr and StdOut streams produced by the child script. Here's an example:

Class CMain
  Private oShell
  Private oExec

  Private Sub Class_Initialize()
    Set oShell = CreateObject("WScript.Shell")
    Set oExec = oShell.Exec("cmd.exe /C cscript.exe " & _
      " //e:vbscript //nologo C:\child.vbs")
  End Sub

  Private Sub Class_Terminate()
    Set oExec = Nothing
    Set oShell = Nothing
  End SUb

  Public Sub Go()
    Do While oExec.Status = 0
      WScript.Sleep 100
    Loop

    Dim e : e = oExec.StdErr.ReadAll
    Dim s : s = oExec.StdOut.ReadAll
    WScript.Echo "Errors reported by child script: " & e
  End Sub
End Class

Dim Main : Set Main = New CMain : Main.Go : Set Main = Nothing

Hope this helps.

Meindert MeindertsmaMeindert Sunday, March 13, 2011 8:12:13 PM

Nice construct indeed! But an anonymous object will do the job as well. Spare yourself a variable and a few keystrokes by replacing:
Dim Func1 : Set Func1 = New CFunc1 : Set Func1 = Nothing

with:
With New CFunc1 : End With

Meindert MeindertsmaMeindert Sunday, March 13, 2011 8:45:41 PM

Had some bad experiences with oShell.Exec:
While oExec.Status = 0
  WScript.StdOut.Write oExec.StdOut.ReadLine
  WScript.Sleep 10
Wend

WScript.StdOut.Write oExec.StdOut.ReadAll
'Do something with oExec.StdErr.ReadAll

The standard output should be visible more or less immediately, while the standard error stream could be sent to a log file after the subprocess had finished. But the whole thing got stuck in the while loop when the subprocess produced more than a few lines of output and errors. I suspect the Readline operation lost the proper place to read from when the stream buffers grew too big.

I'm not too enthousiastic about the Exec facility anymore.

irged Monday, August 8, 2011 3:04:35 AM

Im still getting a runtime error even though I defined a class.


Dim ArgObj, var1, var2, var3
Set ArgObj = WScript.Arguments
var1 = ArgObj(0)
var2 = ArgObj(1)
var3 = ArgObj(2)

Class CFunc1
Private Sub Class_Initialize
WScript.Echo "Starting"
Set objExcel1 = CreateObject("Excel.Application")
objExcel1.DisplayAlerts = false
Set objWorkbook1 = objExcel1.Workbooks.Open(var1)

Set objWorkbook2 = objExcel1.Workbooks.Open(var2)
objExcel1.Run (var3)

objWorkbook1.Save
objExcel1.Quit

Set objExcel1 = Nothing
set ArgObj = Nothing
MsgBox "Should not see this"
End Sub : Private Sub CatchErr : If Err.Number = 0 Then Exit Sub
Select Case Err.Number
Case 6 WScript.Echo "Overflow handled!"
Case Else WScript.Echo "Unhandled error " & Err.Number & " occurred."
End Select
Err.Clear : End Sub : Private Sub Class_Terminate : CatchErr
WScript.Echo "Exiting"
End Sub
End Class
Dim Func1 : Set Func1 = New CFunc1 : Set Func1 = Nothing

Lee HarveyLee_Harvey Monday, August 8, 2011 3:32:14 AM

Just glancing, it appears the CFunc1 class is never initialized properly. Hence, the remaining subroutines (besides Class_Terminate), have virtually no impact.

Thus, try creating a new Public method (eg, Public Sub Main), then place most of your core class logic inside it, and call this new public method at the end. For example:

With New CFunc1 : Call .Main() : End With

I typically try to keep the code inside Private Sub Class_Initialize tight -- leaving little room for error -- just to ensure the custom class gets created/initialized properly.

Hope this helps.

Lee HarveyLee_Harvey Monday, August 8, 2011 3:35:18 AM

Originally posted by Meindert:

Nice construct indeed! But an anonymous object will do the job as well. Spare yourself a variable and a few keystrokes by replacing:

Dim Func1 : Set Func1 = New CFunc1 : Set Func1 = Nothing

with:
With New CFunc1 : End With



Great tip, btw! Thanks.

Write a comment

New comments have been disabled for this post.