🚀The world's best VBA AI has evolved. ExcelMaster is now an autonomous Agent.Read more →
Back to Blog

VBA ByRef vs ByVal in Excel — Why Your Variable Changed After a Call

|

VBA ByRef vs ByVal in Excel — Why Your Variable Changed After a Call

Tested on: Excel 365 v2509 · Excel 2021 · Excel 2019 · last verified 2026-06-07

TL;DRByRef passes the original variable, so a procedure can change it. ByVal passes a copy, so the original is safe. VBA defaults to ByRef — which is why a variable can change out from under you after you call a procedure:

Sub Demo()
    Dim n As Long: n = 10
    AddOne n
    MsgBox n            ' 11 — n really changed, because ByRef is the default
End Sub

Sub AddOne(ByRef x As Long)   ' ByRef: x IS n — writing to x writes to n
    x = x + 1
End Sub
' Change "ByRef" to "ByVal" above and MsgBox shows 10 — the original is untouched.

That default — ByRef when you don't say otherwise — is the source of nearly every "why did my variable change?" bug in VBA. Here's how to never be surprised by it again.

The mental model: original vs photocopy

When you pass a variable to a procedure, you're handing the procedure access to your data. There are two ways to hand it over:

  • ByRef = handing over your actual notebook. The procedure writes on the real pages. When it gives the notebook back, your changes are there. (Ref = reference = "a pointer to the original.")
  • ByVal = handing over a photocopy of one page. The procedure can scribble all over the copy; your notebook never changes. (Val = value = "a snapshot of the contents.")

Same idea, two consequences. With ByRef, changes inside the procedure stick. With ByVal, they vanish the moment the procedure ends. Everything else is detail.

The one rule: the default is ByRef, and that's the trap

This is the fact that bites people: if you don't write ByVal, VBA uses ByRef. These two declarations are identical:

Sub Process(x As Long)        ' no keyword...
Sub Process(ByRef x As Long)  ' ...means exactly this

So every unmarked parameter you've ever written is silently pass-by-reference. Most of the time it doesn't matter — but the day a helper procedure quietly increments the loop counter you passed it, you get a bug that looks impossible: the variable changed and the line that changed it is in a different procedure. Coming from Python, Java, or C# (which pass primitives by value), this feels backwards, and it should — VBA is the odd one out.

The habit that prevents the whole class of bug: write ByVal explicitly on every parameter, and only switch to ByRef when you genuinely mean "send a value back." Your future self debugging at 6 p.m. will thank you.

' Safe by default — callers' variables are protected:
Sub LogRow(ByVal rowNum As Long, ByVal label As String)
    Debug.Print rowNum & ": " & label
End Sub

Using ByRef on purpose: more than one answer

ByRef isn't a mistake to avoid — it's a tool. A Function returns exactly one value. When you genuinely need a procedure to send back two or more results, ByRef output parameters are the clean, classic way:

Sub MinMax(data As Range, ByRef lo As Double, ByRef hi As Double)
    lo = Application.Min(data)
    hi = Application.Max(data)
End Sub

Sub UseIt()
    Dim low As Double, high As Double
    MinMax Range("A1:A100"), low, high   ' fills BOTH variables
    MsgBox "Range: " & low & " to " & high
End Sub

MinMax returns nothing through its name, yet hands back two numbers by writing into the caller's low and high. That's ByRef doing exactly the job it was designed for. The rule of thumb: ByVal for inputs, ByRef for outputs. Mark them deliberately and the signature documents itself.

The gotcha that flips ByRef into ByVal: extra parentheses

This one is genuinely sneaky and connects straight back to how you call a Sub. Wrapping an argument in extra parentheses forces it to be evaluated as an expression first — which means VBA passes the result, i.e. a copy, defeating ByRef:

AddOne n        ' ByRef as intended → n becomes 11
AddOne (n)      ' the (n) is evaluated first → a COPY is passed → n stays 10
Call AddOne(n)  ' here the brackets belong to Call → ByRef again → n becomes 11

So AddOne (n) and Call AddOne(n) look almost identical but behave oppositely. If a ByRef parameter mysteriously refuses to update the caller's variable, look for a stray pair of parentheses around the argument. This is also a deliberate trick: when you want to protect a variable but the procedure is ByRef, wrapping it in ( ) is a quick local override.

The nuance everyone gets wrong: objects ignore the difference

ByVal protects simple values — numbers, strings, booleans, dates. It does not deep-copy an object. With a Range, Worksheet, or Workbook, even ByVal passes a copy of the reference, and that copy still points at the same live object:

Sub Clear(ByVal rng As Range)   ' ByVal, yet...
    rng.ClearContents           ' ...this really clears the caller's cells
End Sub

People reach for ByVal expecting it to "protect" a Range and are baffled when the cells still get wiped. The truth: ByVal on an object only stops you from re-pointing the variable to a different object inside the procedure — it never protects the object's contents. If you want untouched data, copy the values out yourself; the keyword won't do it for you.

Common ByRef / ByVal mistakes (and the fix)

Symptom Cause Fix
A variable changed after calling a procedure Default ByRef let the procedure mutate it Declare the parameter ByVal, or stop modifying it inside
ByRef parameter won't update the caller Argument wrapped in extra parens: MySub (x) Remove the parentheses (or use Call MySub(x))
"ByRef argument type mismatch" Caller's variable type ≠ the declared parameter type Match the types, or pass ByVal so VBA can coerce a copy
ByVal Range still got modified Objects pass a reference even ByVal Copy the values out before the call; the keyword can't protect contents
Need two return values, used globals A Function returns only one value Use ByRef output parameters — the intended tool
Recursion runs out of memory Huge value passed ByVal is copied each level Pass large arrays ByRef to avoid copying every call

When passing data around is the chore — describe the result instead

ByRef/ByVal is the kind of low-level detail that makes VBA feel like a minefield: correct, but fiddly, and one stray parenthesis from a bug. When the actual goal is "split this sheet by region and send each manager their slice," you shouldn't be reasoning about how arguments are copied. ExcelMaster Agent takes the goal in plain English and produces the result — no parameters, no passing convention, no footguns. Try it free →

FAQ

What is the difference between ByRef and ByVal in VBA? ByRef passes a reference to the original variable, so changes made inside the procedure persist after it returns. ByVal passes a copy, so the original is never affected. The keyword controls whether a procedure can modify the caller's variable.

Is VBA ByRef or ByVal by default? ByRef by default. If you write a parameter without a keyword — Sub Process(x As Long) — VBA treats it as ByRef, meaning the procedure can change the variable you passed in. This is the opposite of most modern languages and a common source of bugs.

Why did my variable change after calling a Sub? Because the parameter was ByRef (the default) and the procedure modified it. To protect the variable, declare the parameter ByVal, which passes a copy. Alternatively, wrap the argument in extra parentheses at the call site — MySub (x) — to force a copy.

When should I use ByRef in VBA? Use ByRef deliberately when a procedure needs to send a value back to the caller — for example, returning two or more results through output parameters. A good rule: ByVal for inputs, ByRef for outputs.

Does ByVal protect a Range or Worksheet object? No. ByVal only copies the reference, not the object, so the copy still points at the same live Range or Worksheet — modifying its contents still affects the caller. ByVal on an object only prevents you from re-assigning the variable to a different object inside the procedure.

What does Call MySub(x) do differently from MySub (x)? With Call, the brackets belong to the Call syntax, so x is still passed ByRef. Without Call, (x) is an expression that gets evaluated first, so a copy is passed — defeating ByRef. They look alike but behave oppositely.