Error Handling in VBScript, Part Three

Error Handling in VBScript, Part Three

  • Comments 21

Apparently I've sparked a discussion amongst the super-geniuses of LtU on various innovative language constructs for handling errors. Fascinating stuff that I'd love to learn more about! But I'll be less highfalutin: no doubt about it, error handling in VBScript is a pain in the rear no matter how you slice it.

This series was inspired in party by an email from a reader who was interested in the philosophy of error handling. Here's an excerpt which contrasts two approaches: (my emphasis)

I think I'm old school in saying that error handling should be very tight. Handle errors where you expect to find them. Everything else is left to fail. I'd rather have a program end in a messy death than to blithely continue on in an unpredictable fashion. Some of my cohorts would rather do broad error handling (whole subroutines or sections of the script). They seem to assume that only the errors they expect will happen. And even if other errors do happen, it's better to have the script finish as best it can than to do nothing at all.

I've talked about this before (at the bottom of the post.) As a professional developer who writes complex C# and C++ code on a large team building a product that will be shipped to millions of people, I agree with the writer. Error handling should be built into the architecture from day one. And of course we have done so in VSTO2 -- we have developed a set of exception classes with error numbers and localizable error strings, and tried to engineer everything so that only the right errors get propagated up to the user.

But I'm working in C# and C++, languages specifically designed for implementing complex software written by large teams. VBScript is not such a language -- it was designed for simple administration and web scripts, where often "muddle on through" is exactly what you want it to do.

For example, I have a simple script much like this one that I use for doing quick-and-dirty regular expression searches on my hard disk. You’d better believe that On Error Resume Next is on for that script! If it encounters a directory or file that it cannot search because it is locked by another process, or that I don't have permission to read, or whatever, then I don't want my little twenty-line script to die horribly! I certainly don't want to be constantly maintaining it to add new error logic as I discover more special cases. Were I writing a user-grade bulletproof hard disk searching tool that was going to be shipped in the operating system, you'd better believe I'd have error handling all over that thing, but for my scripty purposes, doing its best and muddling through is almost always plenty good enough. Use the right tool for the job.

Finally, one more implementation detail that I forgot to mention the other day. When VBScript gets certain error numbers back from calls to IDispatch objects, it sometimes takes the error number and replaces it with the equivalent VBScript error number. I have never particularly liked this feature, but VB6 does it, so we're stuck with it for backwards compatibility reasons. This can be a little confusing if you're debugging a problem -- you see one error go out of the object, but a different error is reported to the host. On the off chance that someone finds this useful, I'll put the mapping table that VBScript uses below.

Coming up next time, part two of Riddle Me This, Google, where once more I dole out advice on subjects I know little about -- love, optics and primatology -- based on questions culled from my most recent 29950 Google hits. Stay tuned!

0x80004001 (E_NOTIMPL)                  --> 445 (ActionNotSupported)
0x80004002 (E_NOINTERFACE)              --> 430 (OLENotSupported)
0x80020001 (DISP_E_UNKNOWNINTERFACE)    --> 438 (OLENoPropOrMethod)
0x80020003 (DISP_E_MEMBERNOTFOUND)      --> 438 (OLENoPropOrMethod)
0x80020004 (DISP_E_PARAMNOTFOUND)       --> 448 (NamedParamNotFound)
0x80020005 (DISP_E_TYPEMISMATCH)        -->  13 (TypeMismatch)
0x80020006 (DISP_E_UNKNOWNNAME)         --> 438 (OLENoPropOrMethod)
0x80020007 (DISP_E_NONAMEDARGS)         --> 446 (NamedArgsNotSupported)
0x80020008 (DISP_E_BADVARTYPE)          --> 458 (InvalidTypeLibVariable)
0x8002000A (DISP_E_OVERFLOW)            -->   6 (Overflow)
0x8002000B (DISP_E_BADINDEX)            -->   9 (OutOfBounds)
0x8002000C (DISP_E_UNKNOWNLCID)         --> 447 (LocaleSettingNotSupported)
0x8002000D (DISP_E_ARRAYISLOCKED)       -->  10 (ArrayLocked)
0x8002000E (DISP_E_BADPARAMCOUNT)       --> 450 (FuncArityMismatch)
0x8002000F (DISP_E_PARAMNOTOPTIONAL)    --> 449 (ParameterNotOptional)
0x80020011 (DISP_E_NOTACOLLECTION)      --> 451 (NotEnum)
0x8002802F (TYPE_E_DLLFUNCTIONNOTFOUND) --> 453 (InvalidDllFunctionName)
0x80028CA0 (TYPE_E_TYPEMISMATCH)        -->  13 (TypeMismatch)
0x80028CA1 (TYPE_E_OUTOFBOUNDS)         -->   9 (OutOfBounds)
0x80028CA2 (TYPE_E_IOERROR)             -->  57 (IOError)
0x80028CA3 (TYPE_E_CANTCREATETMPFILE)   --> 322 (CantCreateTmpFile)
0x80030002 (STG_E_FILENOTFOUND)         --> 432 (OLEFileNotFound)
0x80030003 (STG_E_PATHNOTFOUND)         -->  76 (PathNotFound)
0x80030004 (STG_E_TOOMANYOPENFILES)     -->  67 (TooManyFiles)
0x80030005 (STG_E_ACCESSDENIED)         -->  70 (PermissionDenied)
0x80030008 (STG_E_INSUFFICIENTMEMORY)   -->   7 (OutOfMemory)
0x80030012 (STG_E_NOMOREFILES)          -->  67 (TooManyFiles)
0x80030013 (STG_E_DISKISWRITEPROTECTED) -->  70 (PermissionDenied)
0x8003001D (STG_E_WRITEFAULT)           -->  57 (IOError)
0x8003001E (STG_E_READFAULT)            -->  57 (IOError)
0x80030020 (STG_E_SHAREVIOLATION)       -->  75 (PathFileAccess)
0x80030021 (STG_E_LOCKVIOLATION)        -->  70 (PermissionDenied)
0x80030050 (STG_E_FILEALREADYEXISTS)    -->  58 (FileAlreadyExists)
0x80030070 (STG_E_MEDIUMFULL)           -->  61 (DiskFull)
0x800300FC (STG_E_INVALIDNAME)          --> 432 (FileNotFound)
0x80030100 (STG_E_INUSE)                -->  70 (PermissionDenied)
0x80030101 (STG_E_NOTCURRENT)           -->  70 (PermissionDenied)
0x80030103 (STG_E_CANTSAVE)             -->  57 (IOError)
0x80040154 (REGDB_E_CLASSNOTREG)        --> 429 (CantCreateObject)
0x800401E3 (MK_E_UNAVAILABLE)           --> 429 (CantCreateObject)
0x800401E6 (MK_E_INVALIDEXTENSION)      --> 432 (OLEFileNotFound)
0x800401EA (MK_E_CANTOPENFILE)          --> 432 (OLEFileNotFound)
0x800401F3 (CO_E_CLASSSTRING)           --> 429 (CantCreateObject)
0x800401F5 (CO_E_APPNOTFOUND)           --> 429 (CantCreateObject)
0x800401FE (CO_E_APPDIDNTREG)           --> 429 (CantCreateObject)
0x80070005 (E_ACCESSDENIED)             -->  70 (PermissionDenied)
0x8007000E (E_OUTOFMEMORY)              -->   7 (OutOfMemory)
0x80070057 (E_INVALIDARG)               -->   5 (IllegalFuncCall)
0x800706BA (RPC_S_SERVICE_UNAVAILABLE)  --> 462 (ServerNotFound)
0x80080005 (CO_E_SERVER_EXEC_FAILURE)   --> 429 (CantCreateObject)

  • I write complex scripts for the enterprise in cases where no existing tool (or at least one that meets all expectations) can do the job at hand. I agree that vbscript's error handling is a pain, but what are your recommendations enterprise-level error handling those kinds of situations? I'll take a guess and say .NET will be part of your initial response, but let's assume that vbscript will continue to be the language of choice.
  • As a side note: Do you have any articles on Windows Script Components, their pros and cons in today's programming and if they are .NET compatible? Would make a good blog for us die hard vbscripters.
  • I've been meaning to blog on WSCs for some time now, I just haven't gotten around to it yet.
  • My recommendation if you're stuck with VBScript but need to write something big and complicated is to proceed _very_ carefully. There's no magic bullet that I can give you, like "just design your error handlers like this -- blah blah blah blah -- and everything will work out fine."

    The thing that works against you is the scripty nature of the language. The language itself encourages the "Write it. Run it. Tweak it. Ship it." cycle. Slow way down -- factor the program out into functions very early on, carefully define their inputs, outputs and error behaviour, implement them, test them carefully, and then assemble them.

    Yes, that takes a long time and is a lot of work, but that's what you get for choosing to try and build skyscrapers out of wood instead of steel -- it's a lot harder and the end result is a lot more rickety!
  • 8/25/2004 5:40 PM Eric Lippert

    > The language itself encourages the "Write
    > it. Run it. Tweak it.

    Fine so far.

    > Ship it."

    Oops.

    One attribute should be required on this kind of coding, during development. [Unsafe for shipping].

    The language should allow optional insertion of readable error handling, internationalization, etc. Optional for the first three steps and recursions and iterations thereof. But the attribute [Unsafe for shipping] should not be deleteable until these characteristics are present.
  • The REXX scripting language requires that every program begin with a descriptive comment that clearly and correctly documents the purpose of the program.

    However, what the compiler actually ENFORCES is simply that every program begin with a comment. Strangely enough, every REXX program I ever wrote begins with an empty comment.

    Who would have predicted that? :-)

    All kidding aside, you raise several good points. We have languages which recognize that functions, types, security settings, attributes, events and exceptions are first-class objects. I'd love to add "internationalization" to that list (among other things).

    We do have a way in .NET of marking a program "not safe for shipping" -- it's called "delay signing". Since users will typically be unable to use non-strong-named assemblies, it behooves you to not strong-name anything that isn't ready for prime time. Or, put another way, a strong name is your claim that a piece of code is worth putting your name on.

    I have a funny story -- ok, it wasn't funny at the time, as it caused a lot of user pain -- about how a "don't ship this" mark on a piece of code was insufficent to prevent it from being shipped. I'll blog about the internal details of The Great RegExp Debacle some time.
  • I use to work in process control. I have always hated those blind catch all exception handling systems. But I once decided to place one in my code to make sure that the program continued running after a fault. After all, this is process control and the software needs to continue reporting.

    One day I received a crash dump of a problem out in the field. After a week worth of work I still wasn't able to located the bug due to not being able to reproduce the problem. After talking with vendors and the people in charge of the site with the problem, we managed to recreate the problem.

    Unfortunately, recreating the problem didn't help that much. The software was dying in a very strange place. On a lark, I disabled the catch all exception handler and uncovered where the program was really dying. The exception handler was working perfectly and masking another problem.

    After a few more days we finally tracked down the original problem where we were trashing memory when the control system sent our software an output to be sent to a remote device. The memory corruption was trashing our internal data value tables which was causing delayed access violation. This access violation was being eaten by the exception handler. The program would finally die when data values were being sent back to the control system.

    Moral of this story?

    Catching exceptions and not taking corrective actions can lead to invalid data being treated as valid data. Only god knows what might have happened if the program didn't die and we started reporting corrupted data back to the control system.
  • I've just seen this:
    http://support.microsoft.com/default.aspx?scid=kb;en-us;312116&Product=vsnet

    When scripting was blocked, Visual Studio falsely reported that it installed successfully.

    What would everyone's opinion be of a backup program reporting that it successfully made a backup when in fact it hadn't?

    How much fun was it when Internet Explorer used to happily report that it successfully downloading some file when in fact the download had died after 95% or 50% or 5%?

    When an error occurs and can't be corrected, there is something the program needs to do. It needs to inform the user. The user needs to know that they need to either retry the operation and/or get an upgrade and/or try another vendor's program. False reports of success are as malicious as viruses.
  • My belief has always been that good coding is just that - good coding. It doesn't matter what language or platform is. At some point I'll become a .NET believer and switch over. Until then, I'll continue to make sure I build with the most solid wood I can produce.

    I'm sure VS uses script to handle some quick and dirty operations during the install (as alot of apps do), but they chose not to fail gracefully if VS couldn't access the script engine in the first place.
  • What bugs me about "on error resume next" is that it is way too easy to leave on, either by accident or by laziness. Since adding "on error goto 0" makes the program longer, there's an incentive to leave it out for brevity and readability. With scoped error handling such as try/catch you have to at least think about scope. Sure, you can wrap a whole program in try/catch, but at least it makes you think.


  • * Yes, VS uses IE semantics in several places, including install scripts. Drives me nuts. I keep complaining about it, believe me.

    * re: corruption. Yes, if an exception occurs because of a memory fault, you cannot recover and continue safely. I've debugged many, many, many such problems in ASP over the years. We had a specific problem that we kept seeing the symptom of over and over again -- the script engine's language stack would get corrupted, but the code was bulletproof. It was a corruption as you describe.

    This is why ASP has optional process isolation. You pay more per process, but one buggy process cannot corrupt another page.

  • Eric, can you give some insights into why global error handlers weren't implemented in VBScript? It relates to my comments below. :)

    I think the most siognificant limitation we've seen in scripting use is a philosophical one, exacerbated by the limitations of the language.

    Scripting can be legitimately treated as rough programming in some contexts, but when we start looking at large-scale use of scripts for handling lots of arbitrary data, I tend to see the following:
    + The possible range of errors grows astronomically based on lots more possible input types.
    + The number of error encounters grows - lots of batch processing events.
    + The likelihood of bugs increases: limited knowledge of components by the scripters, who are possibly performing ad-hoc composition.
    + The risks of both handled and unhandled errors grow: if you're creating 4000 accounts from a text file, unsuppressed errors can cause a crash that requires all sorts of manual steps to continue the process. Suppressed errors could be even worse, causing things like bogus account creation or arbitrary overwriting of legitimate accounts or user data.

    I think the real skill scripters need is knowing how to perform error triage: good stuff goes through; bad stuff gets checked, and if it is an expected or known issue, it is fixed. If it's not known or fixed, dump it to the user, abort the current processing cycle, and go on to the next one.

    This is kind of hard to implement in VBScript due to the lack of global error handlers, but there are some ways around it; you can locally switch on error suppression, test known problems, and then dump nonconforming data to stderr or a log.

    The most significant limitation in VBScript is that we can't easily say "hey, anywhere we see an error that isn't fixed, send the error info out and start from the beginning". I think that simply comes down to the fact that people weren't thinking about it from the perspective of batch operations, streams, or compositional scripting. Frankly, based on scripting skills I see, I do't know if people would have exploited such features if they HAD been available; the StdIn/StdOut/StdErr capabilities of WSH have been there a long time now and are still rarely used by people who should use them.
  • Your insights are (as usual) spot on Alex. Error handling is hard, and the difficulty grows quickly as the problem scales up. As I've said before and I'll say again, the VBScript language was not designed for programming-in-the-large, it was designed to facilitate rapid development of simple scripts by people who knew a little VB.

    Are there technical reasons why we didn't do global error handling? I wasn't there -- that was before my time -- but I could probably come up with some hand waves.

    Like, it would be quite tricky to make it work in the world where code starts running before all the code has been even downloaded.

    What happens when your handler is in another block? What happens when the engine is moved back to uninitialized state but the block with the error handler isn't marked as persistent? Yes, we have all these problems with subroutines too, but when you call a subroutine that doesn't exist, we raise an error. Error handlers should ideally not afford the possibility of themselves raising errors, because then you have a horrible mess no matter how you slice it.

  • 8/30/2004 10:58 AM Eric Lippert

    > Error handlers should ideally not afford the
    > possibility of themselves raising errors,
    > because then you have a horrible mess no
    > matter how you slice it.

    Exactly the opposite. An error that was not planned for yields a horrible mess no matter how you slice it. Hiding the error yields a compounded horrible mess. An unplanned error in an error handler yields a different compounded horrible mess. Hiding an unplanned error in an error handler yields a multiply compounded horrible mess. Hiding does not make things better, it makes things worse.

    When an error arises in an error handler, do not pretend that the original error was handled, do not hide the error in the error handler, do not go directly to jail until you have first confessed to the user.

    Even if the user isn't a programmer, they deserve to know that their latest 10 hours did not create a reliable backup and they'd better get a different backup tool.
  • Right, I agree with you -- it's a compounded horrible mess. Designing a system which elegantly handles such horrible messes and is still understandable by the typical VBScripter -- who is often a novice or line-of-business programmer -- and is backwards compatible with VB is very difficult. So we didn't even try to add "on error gosub" to VBScript.

    Your point -- that On Error Resume Next is super-dangerous and easily misused -- is well taken. My advice would be for people to not use any language feature which they do not know the semantics of well enough!
Page 1 of 2 (21 items) 12