The 60 VB.NET questions interviewers actually ask, with direct answers, runnable code, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds.
60 questions with answersKey Takeaways
VB.NET is an object-oriented programming language built by Microsoft that runs on the .NET platform. It came out in 2002 as the successor to classic Visual Basic 6, and unlike VB6 it's fully object-oriented, supports inheritance, and shares the Common Language Runtime with C#. Both languages compile to the same intermediate language (IL), so a VB.NET class and a C# class can call each other inside one solution. The syntax favors readable English keywords over braces, which is why it stayed popular for Windows Forms desktop apps, database front ends, and Office macros. In interviews, VB.NET questions probe the .NET object model (value types vs reference types, boxing, the difference between structures and classes), compiler settings like Option Strict and Option Explicit, and how you handle errors with Try/Catch. This page collects the 60 questions that come up most, each with a direct answer and runnable code. If you're building fundamentals, the official Visual Basic documentation from Microsoft is the canonical reference; increasingly the first round is a live AI coding interview, so pair this bank with our AI interview preparation guides for the format side.
Watch: Visual Basic (VB.NET) - Full Course for Beginners
Video: Visual Basic (VB.NET) - Full Course for Beginners (freeCodeCamp.org, YouTube)
Test yourself and earn a certificate
10 quick questions. Score 70%+ to download your VB.NET certificate.
The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.
VB.NET is an object-oriented programming language from Microsoft that runs on the .NET platform. It replaced classic Visual Basic 6 in 2002 and, unlike VB6, it's fully object-oriented with inheritance, interfaces, and access to the shared .NET runtime and its whole base class library.
It isn't source-compatible with VB6; it's a different language that kept the familiar English-style syntax. Code compiles to .NET intermediate language, the same target as C#, so both run on the Common Language Runtime.
Key point: Saying 'it's a rewrite, not an upgrade, of VB6' shows you know the history. Interviewers use this opener to hear how you frame an answer.
Both compile to the same .NET intermediate language and use the same base class library, so capability and performance are near-identical across the two. The differences are syntax and defaults: VB.NET uses English-style keywords and is case-insensitive, while C# uses braces and semicolons and is case-sensitive.
C# has become the dominant .NET language and receives new features first. VB.NET stays common in existing Windows Forms and enterprise systems. Because they share the runtime, projects can mix both languages.
| VB.NET | C# | |
|---|---|---|
| Case sensitivity | Insensitive | Sensitive |
| Statement end | Line break | Semicolon |
| Blocks | End If, End Sub | Curly braces |
| Compiles to | .NET IL | .NET IL (same) |
Key point: The follow-up is usually 'can they call each other?'. Yes, because both produce IL for the same runtime. Have that ready.
Watch a deeper explanation
Video: VB.NET vs C# - A comparison of the two languages, how they are different, and where they are going (IAmTimCorey, YouTube)
A value type holds its data directly and is copied on assignment: Integer, Boolean, Double, Date, and any Structure. A reference type holds a reference to data on the heap, so assignment copies the reference, not the object: Class instances, String, arrays, and Object.
This drives real behavior. Assigning one value type to another makes an independent copy; assigning one reference to another makes two names for the same object, so a change through one is visible through the other.
' value type: independent copy
Dim a As Integer = 5
Dim b As Integer = a
b = 9 ' a is still 5
' reference type: two names, one object
Dim list1 As New List(Of Integer) From {1}
Dim list2 = list1
list2.Add(2) ' list1 now also holds {1, 2}Key point: This sets up boxing and the Structure-vs-Class question. Answer it crisply and expect a follow-up.
A Sub runs code but returns no value; a Function runs code and returns a value with the Return statement or by assigning to the function name. You call a Sub as a statement and use a Function's result in an expression.
Both can take parameters. Choose a Function when the caller needs a result back, and a Sub when the procedure only performs an action.
Sub Greet(name As String)
Console.WriteLine("Hi " & name)
End Sub
Function Square(n As Integer) As Integer
Return n * n
End Function
Greet("Asha")
Dim result = Square(6) ' 36The everyday value types are Integer (32-bit), Long (64-bit), Double and Decimal for numbers, plus Boolean, Char, and Date. The common reference types are String and Object. Each VB.NET keyword is an alias for a .NET type, so Integer is really System.Int32 under the hood.
Decimal matters for money because it avoids the rounding errors of Double's binary floating point. That distinction is a frequent follow-up.
| VB.NET type | .NET type | Category |
|---|---|---|
| Integer | System.Int32 | Value |
| Long | System.Int64 | Value |
| Double | System.Double | Value |
| Decimal | System.Decimal | Value |
| String | System.String | Reference |
Dim declares a variable and, optionally, its type and an initial value, so Dim count As Integer = 0 both names the variable and sets it up. With Option Infer On you can leave the type off and the compiler infers it from the value you assign.
Dim is the local-scope declaration; at class level you'd use access modifiers like Private or Public instead. Declaring the type explicitly is clearer and works well with Option Strict.
Dim count As Integer = 0
Dim name As String = "interview"
Dim total = 42 ' type inferred as Integer with Option Infer OnOption Explicit On requires every variable to be declared before you use it. Option Strict On blocks implicit narrowing conversions and late binding, so you have to write explicit casts. Option Infer On lets the compiler work out a variable's type from the value in its initializer.
Turning Option Strict On is the single best habit for catching type bugs at compile time instead of at run time. Most teams enable Explicit and Strict on every project.
Option Strict On
Option Explicit On
' With Strict On this is a compile error:
Dim n As Integer = "5" ' implicit String -> Integer blocked
Dim n2 As Integer = CInt("5") ' explicit conversion is fineKey point: Saying you keep Option Strict On by default indicates production experience. It's the answer interviewers hope to hear.
Arrays are zero-based and fixed in size once created. Dim scores(4) As Integer makes an array with indexes 0 through 4, so five elements in total. You can initialize inline with braces, and ReDim Preserve resizes an existing array while keeping the values already stored in it.
The size in the declaration is the upper bound, not the count. That off-by-one detail trips people up, so state it clearly.
Dim scores(4) As Integer ' indexes 0..4, five slots
scores(0) = 90
Dim names() As String = {"Asha", "Ben"}
ReDim Preserve names(3) ' grow, keep existing valuesUse the & operator to concatenate strings; it converts its operands to String and avoids the numeric ambiguity that the + operator can cause. For building large strings inside a loop, reach for StringBuilder, because String is immutable and repeated concatenation allocates a brand new string every time.
Interpolated strings ($"Hello {name}") read cleanly for formatting. Common methods include Trim, ToUpper, Substring, and Replace.
Dim name = "Asha"
Dim greeting = "Hi, " & name
Dim interp = $"Score: {90}"
Dim sb As New System.Text.StringBuilder()
For i = 1 To 3
sb.Append(i)
Next
Dim built = sb.ToString() ' "123"If uses If/ElseIf/Else/End If to branch on one or more conditions, running the first block whose condition is true. Select Case tests a single expression against multiple values or ranges and reads far better than a long ElseIf chain when you're matching one value against many options.
Select Case supports ranges (Case 1 To 10), lists (Case 1, 2, 3), and Case Else for the default. Use it when you're comparing one value against many options.
Dim score = 82
Select Case score
Case Is >= 90
Console.WriteLine("A")
Case 80 To 89
Console.WriteLine("B")
Case Else
Console.WriteLine("Below B")
End SelectFor/Next counts through a numeric range with an optional Step value. For Each/Next iterates the items of a collection. Do While/Loop and Do Until/Loop run while or until a condition holds, and you can put the test at the top or the bottom depending on whether the body must run at least once.
Exit For and Exit Do break out early; Continue For and Continue Do skip to the next iteration. Pick For Each when you don't need the index.
For i = 0 To 4 Step 2
Console.WriteLine(i) ' 0, 2, 4
Next
For Each item In {"a", "b"}
Console.WriteLine(item)
Next
Dim n = 3
Do While n > 0
n -= 1
LoopNothing represents the default value of whatever type you assign it to: a null reference for reference types, and the zero value for value types, so 0 for Integer and False for Boolean. Assigning Nothing to an Integer gives you 0, not a null, which surprises people coming from other languages.
Check reference types with Is Nothing or IsNot Nothing, because = doesn't work reliably against Nothing. For nullable value types, use HasValue.
Dim s As String = Nothing
If s Is Nothing Then
Console.WriteLine("no value")
End If
Dim age As Integer = Nothing ' becomes 0, not nullKey point: The trap is claiming Nothing always means null. Point out that value types get their zero value; that shows real understanding.
Console.WriteLine and Console.Write send text to standard output, the difference being the trailing newline. Console.ReadLine reads a whole line of input back as a String. Because console input always arrives as text, convert it with functions like Integer.Parse or CInt whenever you need a number.
The entry point is a Sub Main inside a Module. That's where execution starts for a console application.
Module Program
Sub Main()
Console.Write("Your name: ")
Dim name = Console.ReadLine()
Console.WriteLine($"Hello {name}")
End Sub
End ModuleArithmetic uses +, -, *, and /, plus \ for integer division and Mod for the remainder. Comparison uses =, <>, <, >, <=, and >=. Logical operators are And, Or, Not, and Xor, plus the short-circuiting AndAlso and OrElse that stop evaluating once the result is known.
AndAlso and OrElse stop evaluating once the result is known, which is what you want for guards like If obj IsNot Nothing AndAlso obj.Ready. Plain And and Or always evaluate both sides.
Dim x = 7 \ 2 ' 3, integer division
Dim r = 7 Mod 2 ' 1, remainder
' AndAlso short-circuits, so no null-reference here
If user IsNot Nothing AndAlso user.IsActive Then
Console.WriteLine("active")
End IfKey point: Knowing AndAlso vs And is the point of this question. The short-circuit version prevents a whole class of null-reference bugs.
VB.NET offers conversion functions like CInt, CStr, CDbl, and CDate, plus CType and DirectCast for general conversions between related types. With Option Strict On you must convert explicitly rather than relying on implicit coercion, which is what catches type mistakes at compile time instead of at run time.
For parsing user input safely, TryParse returns a Boolean and avoids exceptions on bad input, unlike Parse which throws. Reach for TryParse when the input might be invalid.
Dim n = CInt("42") ' 42
Dim d = CDbl("3.14") ' 3.14
Dim value As Integer
If Integer.TryParse("abc", value) Then
Console.WriteLine(value)
Else
Console.WriteLine("not a number") ' this runs
End IfConst defines a compile-time constant baked into the code; its value must be known when you compile and can't change. ReadOnly defines a field you can set once, either at declaration or in the constructor, and its value can be computed at run time.
Use Const for true constants like Pi, and ReadOnly when the value depends on constructor arguments or run-time data but shouldn't change after that.
Const MaxRetries As Integer = 3 ' fixed at compile time
Class Config
Public ReadOnly StartedAt As Date
Public Sub New()
StartedAt = Date.Now ' set once, at run time
End Sub
End ClassList(Of T) is the go-to resizable list; Dictionary(Of TKey, TValue) maps keys to values with fast lookup; HashSet(Of T) holds unique items; Queue and Stack give FIFO and LIFO behavior. All are generic, so the element type is checked at compile time.
Prefer these generic collections over the old non-generic ArrayList and Hashtable, which box value types and lose type safety.
Dim names As New List(Of String) From {"Asha", "Ben"}
names.Add("Cara")
Dim ages As New Dictionary(Of String, Integer)
ages("Asha") = 31
Console.WriteLine(ages("Asha")) ' 31A Module holds procedures and variables that are all implicitly Shared, so you call them directly without ever creating an instance. You can't instantiate a Module or inherit from it, and its members are globally accessible within their scope, which makes it the natural home for utility functions.
A Class is a blueprint you instantiate into objects, each with its own state. Use a Module for utility functions and the program entry point; use a Class when you need instances that carry data.
Me refers to the current instance of the class, the same idea as this in C#. You use it to reach the current object's own members, most often to disambiguate a field from a constructor parameter that happens to share the same name, as in Me.balance = balance.
MyBase refers to the base class and MyClass calls the current class's own implementation even when a derived class has overridden it. Those two round out the family.
Class Account
Private balance As Decimal
Public Sub New(balance As Decimal)
Me.balance = balance ' Me disambiguates field from parameter
End Sub
End ClassFor candidates with working experience: object orientation, exception handling, and the standard-library judgment that separates users from understanders.
A Class groups fields, properties, and methods. The constructor is Sub New, which runs when you create an instance with New. Properties expose state through Get and Set accessors, and auto-implemented properties let you write Property Name As String without a backing field.
Encapsulation is by access modifier: Private fields, Public properties. That keeps the internal representation free to change without breaking callers.
Class Employee
Public Property Name As String
Public Property Salary As Decimal
Public Sub New(name As String, salary As Decimal)
Me.Name = name
Me.Salary = salary
End Sub
End Class
Dim e As New Employee("Asha", 90000D)
Console.WriteLine(e.Name)Key point: Using an auto-implemented property instead of a manual Get/Set backing field shows you know modern VB.NET, not just the 2005 style.
Watch a deeper explanation
Video: VB.NET Tutorial For Beginners - Creating Classes (Visual Basic Programming) (VB Toolbox, YouTube)
An auto-implemented property (Public Property Age As Integer) generates a hidden backing field and simple Get/Set for you. A full property declares its own backing field so the Set accessor can validate, transform, or raise events before storing the value.
Reach for the full form the moment you need validation or a side effect. Until then, the auto property keeps the class short.
Class Person
Private _age As Integer
Public Property Age As Integer
Get
Return _age
End Get
Set(value As Integer)
If value < 0 Then Throw New ArgumentException("age >= 0")
_age = value
End Set
End Property
End ClassA class inherits with the Inherits keyword and gains the base class's members. Mark base methods Overridable so a subclass can replace them with Overrides, and call the base version with MyBase. VB.NET has single inheritance for classes but a class can implement many interfaces.
NotInheritable seals a class so nothing can derive from it, and MustInherit makes an abstract base that can't be instantiated on its own.
Class Animal
Public Overridable Function Speak() As String
Return "..."
End Function
End Class
Class Dog
Inherits Animal
Public Overrides Function Speak() As String
Return "Woof"
End Function
End ClassAn interface declares members with no implementation, and a class promises to provide them using the Implements keyword. Each implementing member also carries its own Implements clause naming the exact interface member it satisfies, which is a VB.NET-specific detail that C# doesn't require of you.
Interfaces enable polymorphism without inheritance and are how you code to a contract. A class can implement several interfaces even though it can inherit only one class.
Interface IShape
Function Area() As Double
End Interface
Class Circle
Implements IShape
Private r As Double
Public Sub New(r As Double)
Me.r = r
End Sub
Public Function Area() As Double Implements IShape.Area
Return Math.PI * r * r
End Function
End ClassKey point: The per-member Implements clause is the VB.NET quirk the key signal is. C# doesn't need it, so it matters.NET time.
Overloading defines several methods with the same name but different parameter lists, and the compiler picks the right match from the arguments you pass at the call site. You can mark them with the Overloads keyword or just define them, since VB.NET infers overloading from the differing signatures on its own.
Overloading differs from overriding: overloading is compile-time selection among signatures in the same scope, overriding is run-time replacement of an inherited method.
Function Area(side As Double) As Double
Return side * side
End Function
Function Area(width As Double, height As Double) As Double
Return width * height
End Function
Area(4) ' calls the first
Area(3, 5) ' calls the secondTry wraps code that might fail; Catch handles specific exception types; Finally always runs, whether or not an exception was thrown, so it's where cleanup goes. Catch the most specific types first, and avoid a bare Catch that swallows everything.
VB.NET also has Catch ... When for a filter and Throw to re-raise. Use Throw on its own to preserve the original stack trace rather than Throw ex.
Try
Dim n = Integer.Parse(userInput)
Catch ex As FormatException
Console.WriteLine("not a number")
Catch ex As OverflowException
Console.WriteLine("too big")
Finally
Console.WriteLine("always runs")
End TryHow a Try/Catch/Finally block executes
If no Catch matches, the exception propagates up the call stack, but Finally still runs first.
Key point: Mentioning that plain Throw keeps the original stack trace, while Throw ex resets it, is a senior-sounding detail the technical value is.
Watch a deeper explanation
Video: VB.NET Beginner Tutorial - Error Handling With TRY / CATCH (Visual Basic .NET) (VB Toolbox, YouTube)
Inherit from System.Exception (or a more specific base), name it with an Exception suffix, and provide constructors that pass a message and inner exception to the base. Throw it where the failure happens so callers can catch it by type.
Chaining with the inner exception keeps the root cause in the stack trace, which is what makes production logs useful.
Class PaymentException
Inherits Exception
Public Sub New(message As String, inner As Exception)
MyBase.New(message, inner)
End Sub
End Class
Try
ChargeCard()
Catch ex As TimeoutException
Throw New PaymentException("gateway timed out", ex)
End TryUsing acquires an object that implements IDisposable and guarantees its Dispose runs when the block ends, even if an exception is thrown. It replaces a fragile Try/Finally with one line and is the right pattern for files, database connections, and streams.
The resource is scoped to the block, so it's released as soon as you're done. Forgetting to dispose these is a common source of file locks and connection-pool exhaustion.
Using reader As New IO.StreamReader("data.txt")
Dim line = reader.ReadLine()
Console.WriteLine(line)
End Using ' reader.Dispose() runs here, even on errorByVal passes a copy of the argument's value, so reassigning the parameter inside the method doesn't affect the caller at all. ByRef passes a reference to the caller's variable, so reassigning the parameter changes the caller's variable too. ByVal is the default in VB.NET when you write no keyword.
The subtlety: with a reference type passed ByVal, you still get a copy of the reference, so mutating the object is visible to the caller, but reassigning the parameter to a new object is not.
Sub Bump(ByRef n As Integer)
n += 1
End Sub
Sub NoBump(ByVal n As Integer)
n += 1 ' local copy only
End Sub
Dim x = 5
Bump(x) ' x is now 6
NoBump(x) ' x is still 6Key point: The gotcha follow-up is 'what about a List passed ByVal?'. You can still Add to it; you just can't reassign it. Have that answer ready.
A The technical sequence is a value type: copied on assignment, usually stored inline, and it can't inherit from another type. A Class is a reference type: copied by reference, allocated on the heap, and supports inheritance. Both can have fields, properties, and methods.
Use a Structure for small, immutable value-like data (a point, a money amount) where copy semantics are what you want. Use a Class for entities with identity and shared state.
| Structure | Class | |
|---|---|---|
| Type category | Value type | Reference type |
| Assignment | Copies the value | Copies the reference |
| Inheritance | Not supported | Supported |
| Best for | Small immutable values | Entities with identity |
Boxing wraps a value type in a heap object so it can be treated as an Object or interface reference; unboxing extracts the value back out with a cast. It happens implicitly when you store an Integer in an Object variable.
Boxing costs an allocation and a copy, so heavy boxing in a hot loop hurts performance. Generic collections like List(Of Integer) avoid it, which is one reason to prefer them over the old ArrayList.
Dim n As Integer = 42
Dim boxed As Object = n ' boxing: heap allocation
Dim back As Integer = CInt(boxed) ' unboxing
' List(Of Integer) stores ints directly, no boxing
Dim nums As New List(Of Integer) From {1, 2, 3}LINQ (Language Integrated Query) lets you query collections, databases, and XML with one consistent syntax written right in the language. VB.NET supports both query syntax (From ... Where ... Select) and method syntax (.Where(...).Select(...)), and you pick whichever reads more clearly for the query at hand.
Queries are usually deferred: they don't run until you enumerate the result, so calling ToList forces execution. LINQ replaces manual loops for filtering, projecting, grouping, and ordering.
Dim nums = {5, 12, 8, 130, 44}
' query syntax
Dim big = From n In nums
Where n > 10
Select n
' method syntax
Dim doubled = nums.Where(Function(n) n > 10).Select(Function(n) n * 2)Key point: deferred execution (the query runs when you enumerate, not when you write it) is the detail that matters.
Watch a deeper explanation
Video: Visual Basic Programming - Using LINQ to Make Queries (Christian Hur, YouTube)
A delegate is a type-safe reference to a method with a matching signature, so you can pass methods around like data, store them, and call them later. AddressOf gets a reference to a named method; you can also point a delegate at a lambda.
Delegates are the foundation of events and callbacks. Multicast delegates can reference several methods that all run when the delegate is invoked.
Delegate Function MathOp(a As Integer, b As Integer) As Integer
Function Add(a As Integer, b As Integer) As Integer
Return a + b
End Function
Dim op As MathOp = AddressOf Add
Console.WriteLine(op(3, 4)) ' 7A class declares an event with the Event keyword, raises it with RaiseEvent, and consumers subscribe to be notified. VB.NET gives you two styles: the declarative WithEvents field paired with a Handles clause, and the explicit AddHandler that wires up a handler at run time whenever you choose.
Events are how objects notify others without knowing who's listening, which is the backbone of Windows Forms UI programming. AddHandler is the flexible option because you can attach and detach handlers dynamically.
Class Button
Public Event Clicked()
Public Sub Press()
RaiseEvent Clicked()
End Sub
End Class
Dim b As New Button()
AddHandler b.Clicked, Sub() Console.WriteLine("clicked")
b.Press() ' prints "clicked"A lambda is an inline anonymous function you define right where you need it. VB.NET has expression lambdas (Function(x) x * 2) that return a value, and statement lambdas (Sub(x) ... End Sub) that run a block of statements. They show up constantly with LINQ queries and event handlers.
Lambdas close over variables in their enclosing scope, so they can capture and use local state. That capture is what makes them handy for callbacks with baked-in context.
Dim square = Function(x As Integer) x * x
Console.WriteLine(square(5)) ' 25
Dim names = {"Asha", "Ben", "Cara"}
Dim shortOnes = names.Where(Function(n) n.Length <= 3)A nullable value type (Integer? or Nullable(Of Integer)) lets a value type also hold Nothing, which is useful for database columns and optional values that need to tell 'zero' apart from 'no value at all'. Check HasValue before reading Value, or use GetValueOrDefault to supply a fallback safely.
This solves the problem that a plain Integer can't tell the difference between an unset value and a real 0. The ? suffix is the shorthand for Nullable(Of T).
Dim age As Integer? = Nothing
If age.HasValue Then
Console.WriteLine(age.Value)
Else
Console.WriteLine("unknown") ' this runs
End If
Dim safe = age.GetValueOrDefault(0) ' 0Generics let you write a class or method that works with any type while keeping compile-time type safety, using (Of T) for the type parameter. List(Of T) and Dictionary(Of TKey, TValue) are generic collections. Your own code can be generic too.
The payoff over Object-based code is real type checking and no boxing of value types. You catch type errors at compile time instead of getting a run-time cast failure.
Function FirstOrDefault(Of T)(items As List(Of T)) As T
If items.Count > 0 Then Return items(0)
Return Nothing
End Function
Dim n = FirstOrDefault(New List(Of Integer) From {10, 20}) ' 10A Namespace groups related types together to avoid name collisions and to organize a codebase, like System.IO or your own Company.Billing hierarchy. Imports brings a namespace into scope so you can refer to its types by their short name instead of typing the full dotted path every single time.
Project-level imports set in the project file save you from repeating common Imports in every file. Without the import you'd write System.Text.StringBuilder every time.
Imports System.Text
Namespace Billing
Class Invoice
Public Sub Render()
Dim sb As New StringBuilder() ' short name thanks to Imports
End Sub
End Class
End NamespaceMustInherit marks an abstract class that can't be instantiated on its own; you have to derive from it. MustOverride marks a member with no body that every non-abstract subclass must override. Together they define a base class that provides some behavior and forces the rest.
The difference from an interface is that an abstract class can carry shared fields and implemented methods, and a class inherits only one of them, while it can implement many interfaces. Reach for an abstract class when subclasses share real code, and an interface when you only need a contract.
MustInherit Class Shape
Public MustOverride Function Area() As Double
Public Function Describe() As String ' shared, implemented
Return $"Area is {Area()}"
End Function
End Class
Class Square
Inherits Shape
Private side As Double
Public Sub New(s As Double)
side = s
End Sub
Public Overrides Function Area() As Double
Return side * side
End Function
End ClassKey point: The clean line is 'abstract class for shared behavior, interface for a pure contract'. the key point is you pick based on that, not habit.
advanced rounds probe the runtime, design judgment, and production scars. Expect every answer here to draw a follow-up.
The VB.NET compiler translates your source into Common Intermediate Language (IL) plus metadata, packaged together into an assembly. At run time the Common Language Runtime's just-in-time compiler turns that IL into native machine code, method by method as each is first called, and then executes it on the CPU.
Because C# compiles to the same IL and metadata, VB.NET and C# assemblies interoperate freely. The CLR also provides garbage collection, type safety, and exception handling for both.
Key point: The point of this question is whether you know VB.NET is compiled to IL, then JIT-compiled, not interpreted line by line. State that clearly.
The .NET garbage collector reclaims managed heap memory automatically using a generational, mark-and-sweep approach: short-lived objects sit in generation 0 and get collected often, and survivors get promoted to older generations that are checked less frequently. You don't free managed memory by hand the way you would in C or C++.
The GC doesn't know about unmanaged resources like file handles and connections, so those types implement IDisposable and you release them with Using or an explicit Dispose. The Dispose pattern, with an optional finalizer as a backstop, is the standard way to handle both.
Class FileWrapper
Implements IDisposable
Private stream As IO.FileStream
Public Sub New(path As String)
stream = IO.File.OpenRead(path)
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
stream?.Dispose()
End Sub
End ClassAn Async method can Await a Task, freeing the calling thread while the awaited operation runs. It's for I/O-bound work like network calls and file access, where the thread would otherwise sit idle waiting. The method returns a Task or Task(Of T) so callers can await it too.
The rules that follow: await async all the way up rather than blocking with .Result, which can deadlock on UI threads, and don't use Async for CPU-bound work, where a background thread fits better.
Async Function FetchAsync(url As String) As Task(Of String)
Using client As New Net.Http.HttpClient()
Return Await client.GetStringAsync(url)
End Using
End Function
' caller
Dim body = Await FetchAsync("https://example.com")What Await does when it hits an I/O operation
Nothing new runs in parallel here; the thread is reused for other work while the I/O waits.
Key point: The follow-up trap is 'why not call .Result to get the value?'. Because blocking on an async call can deadlock the UI thread. The that.
Watch a deeper explanation
Video: Is C# better than VB.NET? Is VB.NET even relevant in 2025? (Gavin Lon, YouTube)
Raw Threads give the most control but the most overhead and complexity, so they're rarely the first choice now. The Task Parallel Library (Task.Run, Parallel.For) handles CPU-bound parallel work over a managed thread pool. Async/Await handles I/O-bound concurrency without tying up threads.
The honest coverage names the cost side: shared state across threads needs synchronization (SyncLock, concurrent collections), and mixing blocking and async code is where deadlocks and thread-pool starvation come from.
SyncLock takes a lock on a private reference-type object so only one thread runs the guarded block at a time, which protects shared mutable state. Lock on a dedicated private object, never on Me or a public object, to avoid deadlocks from outside code locking the same thing.
Better still, prefer the concurrent collections (ConcurrentDictionary) and immutable data where you can, because they remove whole classes of locking bugs. Interlocked handles simple atomic counters without a full lock.
Private ReadOnly _lock As New Object()
Private _count As Integer
Sub Increment()
SyncLock _lock
_count += 1
End SyncLock
End SubKey point: Locking on Me is the classic mistake. Explaining why a private lock object is safer signals you've debugged a real deadlock.
By default, reference types compare by reference identity, meaning are these the exact same object, while value types compare by their contents. The = operator and Object.Equals follow that default behavior unless a type overrides Equals, which is exactly what String does so it compares by content instead.
The Is operator always compares reference identity and can't be overridden, which is why you use Is Nothing for null checks. For custom value-like classes, override Equals and GetHashCode together so equal objects hash the same.
Dim a As New List(Of Integer) From {1}
Dim b As New List(Of Integer) From {1}
Console.WriteLine(a Is b) ' False, different objects
Console.WriteLine(a.SequenceEqual(b)) ' True, same contents
Dim s1 = "hi", s2 = "hi"
Console.WriteLine(s1 = s2) ' True, String compares by valueIf two objects are equal according to Equals, they must return the same GetHashCode value, though the reverse isn't required and unequal objects may collide. Break this contract and objects go missing inside Dictionary and HashSet, because those types bucket items by hash first and only then check for equality.
Practical guidance: override both together, base them on the same immutable fields, and don't include mutable fields in the hash, because a changing hash corrupts the object's position in a hash-based collection.
Class Money
Public ReadOnly Amount As Decimal
Public ReadOnly Currency As String
Public Sub New(a As Decimal, c As String)
Amount = a : Currency = c
End Sub
Public Overrides Function Equals(obj As Object) As Boolean
Dim other = TryCast(obj, Money)
Return other IsNot Nothing AndAlso Amount = other.Amount AndAlso Currency = other.Currency
End Function
Public Overrides Function GetHashCode() As Integer
Return HashCode.Combine(Amount, Currency)
End Function
End ClassAn extension method adds a method to an existing type without modifying that type or subclassing it. You define a Shared method inside a Module, mark it with the Extension attribute from System.Runtime.CompilerServices, and its first parameter is the type you're extending, which becomes the object the method is called on.
LINQ's Where and Select are extension methods on IEnumerable. Use them to add readable helpers, but don't overuse them to hide surprising behavior on well-known types.
Imports System.Runtime.CompilerServices
Module StringExtensions
<Extension()>
Function IsBlank(value As String) As Boolean
Return String.IsNullOrWhiteSpace(value)
End Function
End Module
' usage
Dim empty = " ".IsBlank() ' TrueAn Iterator function returns each value with Yield instead of building a whole collection, so values are produced lazily one at a time and memory stays flat. Execution pauses at each Yield and resumes on the next request from the caller's For Each.
This is how you stream large or infinite sequences: reading a huge file line by line, or generating an unbounded series. The function returns IEnumerable(Of T) and only does work as the consumer pulls items.
Iterator Function Fibonacci() As IEnumerable(Of Integer)
Dim a = 0, b = 1
Do
Yield a
Dim next_ = a + b
a = b : b = next_
Loop
End Function
For Each n In Fibonacci().Take(5)
Console.Write(n & " ") ' 0 1 1 2 3
NextYou define how operators behave for your own type with a Public Shared Operator method, for example Operator + on a Money class so two amounts add together naturally with the + sign. The method takes the operands as parameters and returns the computed result just like any other function.
Overload operators only when the meaning is obvious and matches expectations, like arithmetic on a value-like type. Surprising operator behavior is worse than a named method, so use it sparingly.
Structure Vector2
Public X As Double, Y As Double
Public Sub New(x As Double, y As Double)
Me.X = x : Me.Y = y
End Sub
Public Shared Operator +(a As Vector2, b As Vector2) As Vector2
Return New Vector2(a.X + b.X, a.Y + b.Y)
End Operator
End StructureReflection inspects types, methods, and attributes at run time, and it can create objects or invoke members dynamically through the System.Reflection API even when you don't know the type at compile time. Frameworks lean on it for serialization, dependency injection containers, ORMs, and reading the custom attributes you attach to your code.
The trade-off to name: it's slower than direct calls and bypasses compile-time checks, so reserve it for framework-level code. In hot application paths, cache the reflected members or generate delegates rather than reflecting on every call.
An attribute attaches declarative metadata to code that tools and frameworks read at run time through reflection, so behavior lives right next to the thing it applies to. Built-in examples include Obsolete and Serializable, and you write your own by inheriting from System.Attribute and applying it in angle brackets above a member.
Attributes drive validation frameworks, serialization rules, and routing. The value is that behavior is declared next to the code it applies to, and a framework picks it up without you wiring it manually.
<AttributeUsage(AttributeTargets.Property)>
Class RequiredAttribute
Inherits Attribute
End Class
Class Form
<Required()>
Public Property Email As String
End ClassYes, it can. Garbage collection frees unreferenced objects, but references that outlive their usefulness keep objects alive anyway: event handlers that are never detached, Shared collections that only ever grow, and objects rooted by long-lived caches. Undisposed unmanaged resources add native leaks on top of the managed ones.
Diagnose with a memory profiler (dotMemory, PerfView, the Visual Studio diagnostics tools) to snapshot the heap over time and find what's growing and what roots it. The frequent culprit is a forgotten AddHandler with no matching RemoveHandler.
Key point: Naming the event-handler leak specifically (subscribe without unsubscribe) is the answer that shows you've actually chased one in production.
VB.NET runs on modern .NET, not just the legacy .NET Framework, and Microsoft supports it there, though the focus is stability rather than new language features. Console apps, class libraries, and Windows Forms are supported on current .NET; ASP.NET Core web work is effectively C#-first.
The realistic senior take: greenfield VB.NET is uncommon, most VB.NET work today is maintaining or modernizing existing systems, and knowing the migration path (retarget to modern .NET, or interop with C# libraries) is what the question is really checking.
an inventory and tests around current behavior, because a rewrite with no safety net is how migrations fail comes first. Retarget to modern .NET incrementally where possible, replace deprecated APIs, and turn on Option Strict to surface the type problems hiding in loose old code.
Where the codebase is genuinely stuck, the pragmatic move is wrapping the legacy piece behind a stable interface and building new features in modern .NET or C# beside it, rather than a big-bang rewrite that risks the whole system.
Depend on interfaces rather than concrete classes, and pass dependencies through the constructor instead of newing them up inside a class. That inverts control: the caller (or a container) decides which implementation to supply, which makes classes testable with fakes.
Modern .NET ships Microsoft.Extensions.DependencyInjection, so you register services against interfaces at startup and the container builds the graph. The core idea works with or without a container; the container just automates the wiring.
Interface IClock
Function Now() As Date
End Interface
Class Report
Private ReadOnly _clock As IClock
Public Sub New(clock As IClock) ' injected, not newed inside
_clock = clock
End Sub
Public Function Header() As String
Return $"Generated {_clock.Now()}"
End Function
End ClassUse a test framework like xUnit, NUnit, or MSTest with a separate test project. Each test arranges inputs, acts on the unit, and asserts the result; run them from the CLI or the test explorer. Mock external boundaries (clock, network, database) with a library like Moq behind interfaces.
A strong answer tests behavior rather than internals, keeps tests fast and isolated, and treats dependency injection as what makes units testable in the first place.
Imports Xunit
Public Class PricingTests
<Fact>
Public Sub Discount_reduces_price()
Dim result = Pricing.ApplyDiscount(100D, 10)
Assert.Equal(90D, result)
End Sub
End ClassMeasure first with a profiler (Visual Studio diagnostics, dotTrace, PerfView) on a realistic workload, because guesses about the bottleneck are usually wrong. Then fix in order of impact: better algorithms, cutting boxing and unnecessary allocations, batching I/O and database round trips, and only then parallelism.
The discipline is what's evaluated: profile, don't guess, and confirm each change with a measurement. For .NET specifically, watch string concatenation in loops (use StringBuilder), N+1 database queries, and excessive garbage from short-lived objects.
Key point: The escalation ladder (algorithm, allocations, I/O, then parallelism) matters more than naming any single profiler.
Clarify the requirements first (per-user or global, burst tolerance, single process or distributed), then pick the algorithm. A token bucket gives a smooth rate with burst capacity and is a few lines: track tokens and a timestamp, refill by elapsed time, and allow a request only if a token is available.
Single process: keep buckets in a ConcurrentDictionary keyed by user and guard each bucket. Distributed: the state moves to something shared like Redis, because per-process memory can't see global traffic. Close on the edges: what to return on rejection and metrics on rejects.
Class TokenBucket
Private ReadOnly rate As Double, capacity As Double
Private tokens As Double
Private updated As Date
Public Sub New(rate As Double, capacity As Double)
Me.rate = rate : Me.capacity = capacity
tokens = capacity : updated = Date.UtcNow
End Sub
Public Function Allow() As Boolean
Dim now = Date.UtcNow
tokens = Math.Min(capacity, tokens + (now - updated).TotalSeconds * rate)
updated = now
If tokens >= 1 Then
tokens -= 1
Return True
End If
Return False
End Function
End ClassOverrides replaces an Overridable base method so the derived version runs even when called through a base-class reference, which is real polymorphism. Shadows hides a base member with a new one, and which version runs depends on the compile-time type of the reference, not the actual object.
The gotcha this creates: with Shadows, calling through a base reference runs the base member, while calling through a derived reference runs the derived one. That surprises people, so prefer Overrides for polymorphic behavior and reach for Shadows rarely and deliberately.
Class Base
Public Overridable Function Name() As String
Return "base"
End Function
End Class
Class Over
Inherits Base
Public Overrides Function Name() As String
Return "over"
End Function
End Class
Dim b As Base = New Over()
Console.WriteLine(b.Name()) ' "over" because Overrides is polymorphicKey point: If a candidate reaches for Shadows to solve a normal polymorphism need, that's a red flag. Knowing Overrides is the default answer here matters.
VB.NET and C# are the two main languages on .NET, and this is the comparison interviewers reach for first. They compile to the same IL and use the same base class library, so performance and capability are near-identical for most work. The real differences are syntax and defaults: VB.NET uses English-style keywords and is case-insensitive, C# uses braces and is case-sensitive. C# has become the dominant .NET language and gets new features first, while VB.NET holds on in existing Windows Forms and enterprise codebases. Knowing that both share a runtime, and that migration between them is realistic because of the shared IL, is the answer that indicates understanding rather than habit.
| Aspect | VB.NET | C# |
|---|---|---|
| Syntax style | English keywords (If/End If, Sub) | Braces and semicolons |
| Case sensitivity | Case-insensitive | Case-sensitive |
| Runtime and IL | Compiles to .NET IL | Compiles to the same .NET IL |
| Language momentum | Stable, few new features | Gets new .NET features first |
| Common use today | Legacy WinForms, enterprise apps | New .NET, web, cross-platform |
Prepare in layers, and practice out loud. Most VB.NET rounds move from concept questions to live coding to a debugging or design discussion, so rehearse each stage rather than only reading answers.
The typical VB.NET interview flow
Earlier rounds increasingly run as AI coding interviews. The concepts on this page are what gets probed at every stage.
10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.
Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages for 5,000+ hiring teams. These VB.NET questions and scoring notes reflect what actually gets asked and evaluated inside the interviews we host.
See how the AI Coding Interviewer works
Q15. How do you write comments in VB.NET?
A single quote or the REM keyword starts a comment that runs to the end of the line; VB.NET has no block comment syntax. XML documentation comments three quotes above a member and feed IntelliSense and generated docs comes first.
The triple-quote XML comments are worth mentioning because tools read them to build API documentation and editor tooltips.