Sub Procedures
Organise your VB 2026 code into reusable, named Sub procedures — no-parameter Subs, ByVal vs ByRef parameters, Optional parameters, ParamArray, procedure scope and access modifiers, the call stack, and how Event handlers are themselves special Sub procedures.
ByVal (the default) when you want to pass a copy of the variable — the caller's variable is protected. Use ByRef when the Sub must modify the caller's variable. Keep Subs small and focused on one responsibility. Name them with a clear verb phrase: ValidateForm, ClearAllFields, DisplayResults.
16.1 What is a Sub Procedure?
A Sub procedure is a named, reusable block of code that performs a specific task. When the Sub finishes, control returns to the line after the call. Unlike a Function (Lesson 17), a Sub does not return a value.
You have already been writing Sub procedures in every lesson — every event handler (btnClick_Click, txtName_TextChanged) is a special Sub procedure that VB calls automatically when the user interacts with the UI.
' Button 1 Click txtName.Clear() txtEmail.Clear() txtPhone.Clear() lblStatus.Text = "" ' Button 2 Click (same code again!) txtName.Clear() txtEmail.Clear() txtPhone.Clear() lblStatus.Text = ""
Private Sub ClearForm() txtName.Clear() txtEmail.Clear() txtPhone.Clear() lblStatus.Text = "" End Sub ' Button 1 Click ClearForm() ' Button 2 Click ClearForm()
Don't Repeat Yourself: if you find yourself copying and pasting the same block of code in two or more places, extract it into a Sub procedure. Changes only need to be made in one place. This is the single most important refactoring habit you can build as a programmer.
16.2 Defining and Calling a Sub
' Define a Sub with no parameters Private Sub DisplayWelcome() lblWelcome.Text = "Welcome to VB 2026!" lblWelcome.ForeColor = Color.DarkBlue lblWelcome.Font = New Font("Segoe UI", 14, FontStyle.Bold) End Sub ' Call the Sub -- use the name followed by parentheses Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load DisplayWelcome() ' called when form opens End Sub Private Sub btnReset_Click(sender As Object, e As EventArgs) Handles btnReset.Click DisplayWelcome() ' called again from button End Sub ' A Sub can call other Subs -- building a pipeline Private Sub ProcessOrder() ValidateOrder() CalculateTotals() UpdateInventory() SendConfirmation() End Sub ' Early exit with Return (like Break for loops) Private Sub SaveRecord() If String.IsNullOrWhiteSpace(txtName.Text) Then lblError.Text = "Name is required." Return ' exit the Sub early End If ' ... rest of save logic only runs if name is filled in End Sub
16.3 Parameters — Passing Data Into a Sub
Parameters allow you to pass data into a Sub so it can work with different values each time it is called, making the Sub truly reusable. Declare parameters inside the parentheses: name As Type.
' Sub with one parameter Private Sub ShowMessage(message As String) lblStatus.Text = message End Sub ShowMessage("File saved successfully.") ShowMessage("Error: connection failed.") ' Sub with multiple parameters Private Sub DisplayStudentInfo(name As String, score As Integer, grade As String) lstResults.Items.Add($"{name,-20} {score,3} {grade}") End Sub DisplayStudentInfo("Alice Lim", 92, "A") DisplayStudentInfo("Bob Tan", 74, "B") DisplayStudentInfo("Carol Wong", 85, "A") ' Parameters vs Arguments ' PARAMETER = variable declared in the Sub definition ' ARGUMENT = actual value passed when calling the Sub Private Sub SetLabelColour(lbl As Label, colour As Color) lbl.BackColor = colour End Sub SetLabelColour(lblName, Color.LightYellow) ' lbl=lblName, colour=LightYellow SetLabelColour(lblEmail, Color.LightPink) ' lbl=lblEmail, colour=LightPink ' Named arguments (VB 2026 supports named arguments for clarity) DisplayStudentInfo(name:="Diana", score:=88, grade:="A") ' named args
16.4 ByVal vs ByRef — The Critical Difference
This is one of the most important concepts in VB 2026. When you pass a variable to a Sub, how it is passed determines whether the Sub can change the caller's original variable.
| Keyword | What is passed | Caller's variable | When to use |
|---|---|---|---|
| ByVal (default) | A copy of the value | Unchanged (protected) | Reading data; most parameters should be ByVal |
| ByRef | A reference to the variable | Modified by the Sub | When the Sub must update the caller's variable |
' ByVal -- caller's variable is NOT changed Private Sub TryDouble(ByVal n As Integer) n = n * 2 ' only changes the local copy End Sub Dim x = 5 TryDouble(x) ' x is still 5 -- ByVal protected it lblResult.Text = x.ToString() ' "5" ' ByRef -- caller's variable IS changed Private Sub ActuallyDouble(ByRef n As Integer) n = n * 2 ' modifies the original variable End Sub Dim y = 5 ActuallyDouble(y) lblResult.Text = y.ToString() ' "10" -- y was modified ' Practical ByRef: swap two values Private Sub Swap(ByRef a As Integer, ByRef b As Integer) Dim temp = a a = b b = temp End Sub Dim p = 10, q = 20 Swap(p, q) lblResult.Text = $"p={p} q={q}" ' "p=20 q=10" ' Practical ByRef: multiple "outputs" when you can't use a Function Private Sub ParseName(fullName As String, ByRef firstName As String, ByRef lastName As String) Dim parts = fullName.Split(" "c) firstName = If(parts.Length > 0, parts(0), "") lastName = If(parts.Length > 1, parts(1), "") End Sub Dim fn = "", ln = "" ParseName("Alice Lim", fn, ln) lblFirst.Text = fn ' "Alice" lblLast.Text = ln ' "Lim"
Enter a starting value, choose ByVal or ByRef, and run the Sub. Watch whether the caller's variable changes — and see the call stack frame appear and disappear.
16.5 Optional Parameters
Optional parameters have a default value. Callers may omit them; if omitted the default is used. All optional parameters must come after all required parameters.
' Optional parameter with a default value Private Sub ShowAlert(message As String, Optional title As String = "Alert", Optional beep As Boolean = False) If beep Then Console.Beep() MsgBox(message, MsgBoxStyle.Information, title) End Sub ' All three ways to call it: ShowAlert("File saved.") ' title="Alert", beep=False ShowAlert("Saved.", "Success") ' beep=False ShowAlert("Low disk space!", "Warning", True) ' all three provided ' More practical: formatted display sub Private Sub AddListItem(lst As ListBox, text As String, Optional prefix As String = "", Optional upperCase As Boolean = False) Dim item = If(prefix <> "", prefix & " ", "") & text lst.Items.Add(If(upperCase, item.ToUpper(), item)) End Sub AddListItem(lstLog, "Server started") ' "Server started" AddListItem(lstLog, "Connection failed", "[ERROR]") ' "[ERROR] Connection failed" AddListItem(lstLog, "disk full", "[WARN]", True) ' "[WARN] DISK FULL"
16.6 ParamArray — Variable Number of Arguments
ParamArray lets a Sub accept any number of arguments of the same type. The arguments are collected into an array inside the Sub. ParamArray must be the last parameter and cannot be combined with Optional.
' ParamArray -- accepts any number of Double values Private Sub ShowStats(ParamArray values() As Double) If values.Length = 0 Then lblStats.Text = "No values provided." : Return End If Dim total = values.Sum() Dim avg = total / values.Length Dim min = values.Min() Dim max = values.Max() lblStats.Text = $"Count={values.Length} Sum={total:F1} Avg={avg:F2} Min={min} Max={max}" End Sub ShowStats(10, 20, 30) ' 3 values ShowStats(5, 15, 25, 35, 45) ' 5 values ShowStats(100) ' 1 value ShowStats() ' 0 values -- handled by If check ' Mixed: required param + ParamArray Private Sub LogMessage(category As String, ParamArray parts() As String) Dim combined = String.Join(" | ", parts) lstLog.Items.Add($"[{category}] {combined}") End Sub LogMessage("INFO", "App started") LogMessage("ERROR", "File not found", "C:\data.txt", "Errno 2") LogMessage("AUDIT", "Login", "admin", "192.168.1.1", "success")
Switch between no-parameter, ByVal, ByRef, Optional, and ParamArray Sub forms. See exactly what each call produces and how the caller's variable is affected.
16.7 Scope and Access Modifiers
Sub procedures have an access modifier that controls which code can call them.
| Modifier | Visible to | Typical use |
|---|---|---|
| Private | Current class/form only | Internal helpers; event handlers (most Subs) |
| Public | Any code in the project | Utility Subs shared across forms |
| Protected | Current class + subclasses | Base class helpers in OOP (Lesson 25) |
| Friend | Same assembly | Subs shared within the project but not outside |
| Shared / Static | Called on the class, not an instance | Utility methods that don't need form state |
' Private -- only Form1 can call this Private Sub UpdateStatusBar(message As String) lblStatus.Text = message lblStatus.ForeColor = Color.DarkGreen End Sub ' Public -- any form in the project can call this Public Sub ClearAllControls(ParamArray controls() As Control) For Each ctrl As Control In controls If TypeOf ctrl Is TextBox Then CType(ctrl, TextBox).Clear() If TypeOf ctrl Is Label Then CType(ctrl, Label).Text = "" Next End Sub ' Shared -- called on class, not on an instance Public Shared Sub LogError(source As String, message As String) System.IO.File.AppendAllText("errors.log", $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {source}: {message}{Environment.NewLine}") End Sub ' Call the Shared sub without creating an instance: FormHelper.LogError("SaveRecord", "Database timeout")
16.8 Event Handlers are Sub Procedures
Every event handler you have written since Lesson 1 is a Sub procedure with a special signature. The Handles clause tells VB which control and event this Sub should respond to. A single Sub can handle multiple events, and a single event can be handled by multiple Subs.
' Standard event handler signature Private Sub btnSave_Click(sender As Object, e As EventArgs) Handles btnSave.Click SaveRecord() ' delegate to a helper Sub End Sub ' One Sub handles multiple events (Handles clause with commas) Private Sub TextBox_TextChanged(sender As Object, e As EventArgs) _ Handles txtName.TextChanged, txtEmail.TextChanged, txtPhone.TextChanged ValidateForm() ' validate whenever any text box changes End Sub ' Shared validation logic -- called from multiple events Private Sub ValidateForm() Dim nameOK = Not String.IsNullOrWhiteSpace(txtName.Text) Dim emailOK = txtEmail.Text.Contains("@") Dim phoneOK = txtPhone.Text.Length >= 8 btnSubmit.Enabled = nameOK AndAlso emailOK AndAlso phoneOK lblStatus.Text = If(btnSubmit.Enabled, "Ready to submit", "Please complete all fields") End Sub ' sender parameter tells you WHICH control raised the event Private Sub Button_Click(sender As Object, e As EventArgs) Handles btn1.Click, btn2.Click, btn3.Click Dim btn = CType(sender, Button) lblClicked.Text = $"You clicked: {btn.Text}" End Sub ' Wire event handler at runtime (without Handles clause) AddHandler btnDynamic.Click, AddressOf btnDynamic_Click RemoveHandler btnDynamic.Click, AddressOf btnDynamic_Click ' remove when no longer needed
See exactly how the call stack builds when Subs call other Subs. Choose a scenario and watch frames push and pop as execution flows through the Sub chain.
16.9 Practical Examples
Example 16.1 — Form with Reusable Validation Sub
Public Class RegistrationForm Private Sub HighlightField(txt As TextBox, lbl As Label, errorMsg As String) txt.BackColor = Color.LightPink lbl.Text = errorMsg lbl.ForeColor = Color.Red lbl.Visible = True End Sub Private Sub ClearFieldError(txt As TextBox, lbl As Label) txt.BackColor = Color.White lbl.Text = "" lbl.Visible = False End Sub Private Sub ValidateForm(ByRef isValid As Boolean) isValid = True If String.IsNullOrWhiteSpace(txtName.Text) Then HighlightField(txtName, lblNameErr, "Name is required.") isValid = False Else ClearFieldError(txtName, lblNameErr) End If If Not txtEmail.Text.Contains("@") OrElse Not txtEmail.Text.Contains(".") Then HighlightField(txtEmail, lblEmailErr, "Enter a valid email.") isValid = False Else ClearFieldError(txtEmail, lblEmailErr) End If End Sub Private Sub btnRegister_Click(sender As Object, e As EventArgs) Handles btnRegister.Click Dim valid As Boolean ValidateForm(valid) ' ByRef: valid gets updated inside sub If Not valid Then Return MsgBox("Registration successful!") End Sub End Class
A complete form demonstrating how Subs work together: ValidateField (ByRef), HighlightError, ClearError, and ResetForm — each responsible for one thing.
Enter a comma-separated list of numbers. The Sub receives them as a ParamArray and computes count, sum, average, min, max, and range.
16.10 GitHub Copilot — Sub Procedure Patterns
' Create reusable validation subs: ValidateRequired(txt, lbl), ValidateEmail(txt, lbl), ValidateRange(txt, lbl, min, max). Each highlights the field pink and shows an error Label. ValidateForm calls all three.'' Reusable validation subs for a registration form. Private Sub ValidateRequired(txt As TextBox, lbl As Label) If String.IsNullOrWhiteSpace(txt.Text) Then txt.BackColor = Color.LightPink lbl.Text = "This field is required." Else txt.BackColor = Color.White lbl.Text = "" End If End Sub Private Sub ValidateEmail(txt As TextBox, lbl As Label) Dim valid = txt.Text.Contains("@") AndAlso txt.Text.Contains(".") _ AndAlso txt.Text.Length >= 5 txt.BackColor = If(valid, Color.White, Color.LightPink) lbl.Text = If(valid, "", "Enter a valid email address.") End Sub Private Sub ValidateRange(txt As TextBox, lbl As Label, min As Integer, max As Integer) Dim n As Integer Dim valid = Integer.TryParse(txt.Text, n) AndAlso n >= min AndAlso n <= max txt.BackColor = If(valid, Color.White, Color.LightPink) lbl.Text = If(valid, "", $"Enter a number between {min} and {max}.") End Sub Private Sub ValidateForm(ByRef isValid As Boolean) ValidateRequired(txtName, lblNameErr) ValidateEmail (txtEmail, lblEmailErr) ValidateRange (txtAge, lblAgeErr, 18, 99) isValid = lblNameErr.Text = "" AndAlso lblEmailErr.Text = "" AndAlso lblAgeErr.Text = "" End Sub
' Write a WriteAuditLog sub with category As String and ParamArray fields() As String. Format as CSV with timestamp and append to audit.csv. Handle IO exceptions silently.'' Audit logger: WriteAuditLog(category, ParamArray fields) Private Sub WriteAuditLog(category As String, ParamArray fields() As String) Try Dim timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") Dim safeCat = category.Replace(","c, ";"c) Dim safeFields = fields.Select(Function(f) f.Replace(","c, ";"c)).ToArray() Dim line = $"{timestamp},{safeCat},{String.Join(",", safeFields)}" System.IO.File.AppendAllText("audit.csv", line & Environment.NewLine) Catch ex As Exception ' Log to debug output but don't crash the app Debug.WriteLine($"WriteAuditLog failed: {ex.Message}") End Try End Sub ' Usage: WriteAuditLog("LOGIN", "admin", "192.168.1.1", "success") WriteAuditLog("SAVE", "record 42", "updated") WriteAuditLog("ERROR", "NullRef", "SaveRecord", "line 87")
Try these in the Copilot Chat panel while writing Sub procedures:
- "Refactor this event handler into smaller Sub procedures, one for validation, one for saving, one for updating the UI"
- "Create a Sub that accepts ByRef min and max As Integer and scans an array to find both in a single pass"
- "Write a Sub called SetControlState(enabled As Boolean, ParamArray controls() As Control) that enables or disables all passed controls at once"
- "Convert all TextBox_TextChanged event handlers on this form to call a shared ValidateForm() Sub with ByRef isValid"
Lesson Summary
- A Sub procedure performs a task without returning a value. Every event handler is a Sub. Define with
Private Sub Name(params) ... End Sub; call withName(args). - ByVal (default) passes a copy — the caller's variable is unchanged. ByRef passes a reference — the Sub can modify the caller's variable. Prefer ByVal unless modification is explicitly needed.
- Optional parameters have a default value and may be omitted by the caller. They must follow all required parameters. Use named arguments (
param:=value) for clarity when skipping optional params in the middle. - ParamArray accepts any number of arguments of the same type, collected into an array. It must be the last parameter. Use it for utility Subs like loggers, formatters, and stats calculators.
- Access modifiers control visibility:
Privatefor form-internal helpers (most Subs),Publicfor shared utilities. TheSharedmodifier lets a Sub be called on the class without an instance. - Event handlers use
Handlesto wire automatically. One Sub can handle multiple events (comma list inHandles). Usesenderto identify which control raised the event. UseAddHandler/RemoveHandlerfor runtime wiring. - Keep each Sub small and focused — one responsibility. If a Sub does more than one thing, split it. Extract repeated code blocks into named Subs immediately (DRY principle).
Exercises
Exercise 16.1 — Form Helper Suite
- Create a form with Name, Email, Age, and Phone TextBoxes
- Write a
Private Sub HighlightError(txt As TextBox, lbl As Label, msg As String)andClearErrorcounterpart - Write
ValidateForm(ByRef isValid As Boolean)that calls the helpers for each field - Wire all TextChanged events to a single handler that calls
ValidateFormand enables/disables the Submit button - Copilot challenge: Ask Copilot to "add a ResetForm Sub that clears all TextBoxes and error labels at once using For Each"
Exercise 16.2 — Swap & Sort
- Write
Sub Swap(ByRef a As Integer, ByRef b As Integer) - Write
Sub BubbleSort(ByRef arr() As Integer)that uses your Swap Sub internally - Enter 5 numbers in TextBoxes, call BubbleSort, display the sorted result in a ListBox showing each pass
- Verify that after calling BubbleSort, the original array variable on the form is sorted (not a copy)
- Copilot challenge: Ask Copilot to "add a Sub SelectionSort alongside BubbleSort and compare their pass counts in a Label"
Exercise 16.3 — Flexible Logger
- Write
Sub Log(level As String, message As String, Optional source As String = "App", Optional showTime As Boolean = True) - Write
Sub LogMany(level As String, ParamArray messages() As String)that callsLogfor each message - Display log entries in a ListBox with colour coding: ERROR = red, WARN = orange, INFO = blue
- Add a Clear button that calls a
ClearLogSub which clears both the ListBox and a status Label - Copilot challenge: Ask Copilot to "add timestamps, save the log to a .txt file, and add a Sub ExportLog(path As String)"
Related Resources
← Lesson 15
Looping — For..Next, For Each, Do While, Do Until.
Lesson 17 →
Functions — procedures that return values.
MS Docs — Sub Procedures
Complete VB.NET reference for Sub procedures, parameters and scoping.
MS Docs — ByVal / ByRef
Detailed explanation of value vs reference parameter passing.
Featured Books
Visual Basic 2022 Made Easy
Hands-on Sub procedure exercises including validation helpers, swap algorithms, and reusable UI utilities.
View on Amazon →
VB Programming With Code Examples
48 complete programs demonstrating modular design with well-named Sub procedures throughout.
View on Amazon →