Practical recommendations for VBA developers


As a programming language, Visual Basic for Applications has a lot of bad design choices. It was born in the early 1990s as a way to script Microsoft Office, and still keeps problems of that era.

Variables do not have to be declared before use, and a single typo can quietly create a brand-new variable instead of raising an error. Almost everything defaults to the loosely typed Variant, so type mistakes surface at runtime rather than at compile time. Error handling relies on On Error and GoTo labels instead of structured exceptions, which makes it easy to hide failures instead of dealing with them. The language passes arguments by reference by default, so a procedure can mutate its caller's data without any obvious sign. On top of that, the tooling is dated: no package manager, no real dependency management, and an editor that has barely changed in decades.

None of this means VBA is useless. It is still the fastest way to automate Office, and millions of critical spreadsheets and business processes depend on it even today. The problem is that the language gives you very little protection against your own mistakes, which means the discipline that other languages enforce for you must instead come from you.

As a developer with more then 6 years of VBA experience, I decided to create a collection of practical recommendations that may help you to write VBA code that stays readable, predictable, and maintainable despite the language's fundamental flaws.

Force yourself to declare every variable

VBA has an important directive: Option Explicit. If you put this line at the top of a module, the VBA compiler will force every variable to be declared. Otherwise, a variable name with a typo is treated as a new variable, which leads to unexpected errors.

For example, this code compiles but fails at runtime. Because Shet is never declared, VBA creates it as an empty Variant, and calling .Range on it raises Object required (error 424):

Public Sub WriteHelloWorld(ByVal Sheet As Worksheet)
    Shet.Range("A1").Value = "Hello, world"  ' Shet is a typo for Sheet
End Sub

With Option Explicit, the same typo is caught before the code even runs, with a Variable not defined compile error:

Option Explicit

Public Sub WriteHelloWorld(ByVal Sheet As Worksheet)
    Shet.Range("A1").Value = "Hello, world"
End Sub

You can enable automatic Option Explicit row creation at the start of every module with Tools → Options → Require Variable Declaration.

Declare a type for everything

Always declare types explicitly where possible. This allows the VBA compiler to reason about your code and perform type checking. A type is also an important marker while developing: in many cases it conveys the programmer's intent even more than the variable name does.

Of course, sometimes you cannot declare a type explicitly. For example, the control variable of a For Each loop must be a Variant (or an object type), so you cannot declare it as Long:

Dim num As Variant

For Each num In numbers
    ' ...
Next num

In this case I usually add a comment that documents the real type:

Dim num As Variant ' Long

Furthermore, I always do this for collection types. VBA has no generics, so the element type of a Collection or Dictionary is invisible; a comment restores that information for the next reader:

' Instead of
Dim dict As Dictionary

' ... I write
Dim dict As Dictionary ' Dictionary<String, Long>

Choose correct integral types

Integer is a bad choice for your numeric type is 99% of cases, because it is a 16-bit integer. This means that it overflows at 32767.

Always use Long: it is the natural 32-bit integer type and is as fast as Integer. On modern systems Integer is stored as 16 bits but offers no performance benefit, so there is rarely a reason to prefer it.

Explicit ByRef and ByVal

When declaring functions' or subs' parameters, explicitly mark it with ByVal or ByRef keyword. Prefer ByVal, because it's an additionali safety measure for your code: callee won't be able to silently mutate the caller's variable.

Naming of modules

Never leave default module, class, and sheet names like Module1, Class1, or Sheet1. VBA allows you to change these names, and for modules and sheets I prefer to prefix each name with its type, for example: ModuleDatabase, SheetPayments.

You change the object name in the Properties window (the (Name) property), not by renaming the tab.

Please note that every worksheet has two names:

  • The tab name, which the user sees and can change, and which Worksheets("...") looks up by text.
  • The VBA object name (also called the code name), which you use directly in code — SheetPayments in our case. It does not break when a user renames the tab, which is exactly why it is worth setting deliberately.

Classes are the exception: I don't prefix them. A class models an entity and is instantiated as a variable, so a simple noun reads better than a prefixed name. Person is clearer than ClassPerson, especially at the call site:

Dim customer As Person

Avoid implicitness

VBA has a lot of implicit functionality. For example, when you write:

amount = Range("A1")

The compiler treats it as the following call:

amount = ActiveSheet.Range("A1").Value

Two things are filled in for you here: .Value (the default property of a Range) and the ActiveSheet qualifier. During execution the active sheet can be changed unintentionally, and you may start reading data from the wrong worksheet.

So I suggest always qualifying these calls explicitly:

  • Pass a worksheet when you use members like Range or Cells.
  • Pass a workbook when you use members like Worksheets("...") or Sheets(1).
amount = wsPayments.Range("A1").Value
Set ws = wbData.Worksheets("Payments")

VBA allows you to directly pass correct objects, so avoiding functionality that emulates user actions like .Select / .Activate / Selection is also a good idea.

Avoid early binding of external libraries

Microsoft Office has very limited repository of external libraries, and I don't recomment to use early bindings to types of these libraries.

For example I don't recomment to do this:

Dim dict As Scripting.Dictionary
Set dict = New Scripting.Dictionary

This code is not bad by itself, but Microsoft Office very often breaks dependencies that you manually add.

Unfortunately, in order to make your code more stable, you should use late binding:

Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")

This solution is not perfect either, because you sacrifice IntelliSense and compile-time type checking.

But you can also write a thin wrapper class around the late-bound object, which restores IntelliSense at every call site while keeping late binding internally.

Better collections classes

The built-in Collection class, used to store a variable-length list of elements, is very limited: it lacks functionality, so you have to reinvent the wheel every time for trivial operations like searching, filtering, or sorting.

I have developed a VBA-List repository: a class built to be compatible with Collection, but adds the higher-level operations the standard Collection is missing.

Scripting.Dictionary is also problematic: it doesn't work on MacOS and requires late binding, as discussed in previous section. So you may use 100% compatible class from VBA-Dictionary repository.

Avoid As New

Don't use As New in a variable declaration. Variables declared this way are auto-instancing: VBA silently re-creates the object the moment you touch the variable again after it has been set to Nothing. This leads to non-obvious behavior that is hard to catch:

Dim a As New List

Dim b As List
Set b = New List

Set a = Nothing
Set b = Nothing

Debug.Print (a Is Nothing) ' False - reading `a` re-created it
Debug.Print (b Is Nothing) ' True

Because evaluating a Is Nothing touches a, VBA instantiates a fresh List before the comparison runs, so you can never observe a as Nothing. Always declare the type and create the instance explicitly with Set ... = New ..., as shown with b.

Correct error handling

Unlike modern language, the only way of error handling in VBA is goto statement (On Error Goto <label>), or just ignoring it (On Error Resume Next).

The pattern below keeps error handling explicit and narrowly scoped. Before each risky statement you put On Error GoTo <label>, after each risky statement - On Error GoTo 0. Every unsafe operation gets its own dedicated handler label, so you always know precisely which statement failed and can react accordingly:

Sub DoSomething()
    ' safe operation

    On Error Goto unsafeOperation1Handler
    ' unsafe operation 1
    On Error Goto 0

    ' safe operation 

    On Error Goto unsafeOperation2Handler
    ' unsafe operation 2
    On Error Goto 0

    'safe operation

    Exit Sub

unsafeOperation1Handler:


    Exit Sub

unsafeOperation2Handler:


    Exit Sub
End Sub

Two details make this schema work:

  • On Error GoTo 0 after each block turns error trapping back off, so the "safe" code between blocks runs without an active handler. Without it, one handler would silently stay in effect and swallow later errors.
  • The Exit Sub before the first label is essential. Handler labels are just ordinary lines of code; without an early Exit Sub, execution would fall through into unsafeOperation1Handler even when no error occurred. Each handler also ends with Exit Sub so one handler never falls through into the next.

You should also avoid On Error Resume Next: use it only if you do not care about result of operation. But even if you do not care about result of operation, always put On Error Goto 0 after it:

On Error Resume Next
' operation that is allowed to fail
On Error Goto 0

'safe operation

Don't be afraid of the macro recorder

The macro recorder is your friend. It works as living documentation for every piece of Excel functionality: you record a macro while performing an action by hand, and VBA generates the code that reproduces it. When you don't know which object or method does something, the recorder shows you.

But the code it generates is horrible — always refactor it. The recorder writes everything in terms of .Select, .Activate, and Selection, against the active sheet — exactly the implicit, click-dependent style this article warns against. Treat its output as a hint about which members to call, then rewrite it with explicit object references:

' Recorded
Range("A1").Select
Selection.Value = 10

' Refactored
wsPayments.Range("A1").Value = 10

Unit testing

With vba-test library you may write unit tests for your functionality developed in VBA.

Custom Ribbon

Instead of a plain menu of buttons, MS Office allows you to design custom ribbons whose buttons can trigger your VBA code. It is the professional way to give users an entry point to your macros.

The ribbon is defined by a customUI XML part stored inside the workbook file. Excel has no built-in editor for it, so it is usually edited with a third-party tool such as the Office RibbonX Editor. Each button's onAction attribute names the VBA callback that runs when it is clicked.

Conclusion

Unfortunately, VBA macros have become a synonym for "unmaintainable, messy code" — because of the language design, the many developers who never learned to write it well, the lack of tooling, and tons of legacy code.

To write good VBA code, you have to follow strict hygiene rules. This set of rules I created the hard way — by stepping on every rake in the yard, so you don't have to. Follow them, and VBA becomes a surprisingly productive tool instead of a mess waiting to happen.


Tags: programming vba

Creation time: 2026-07-03

Last modification time: 2026-07-03