PONYλM2Modula-2

Visual Basic.CodeCompared.To/JavaScript

An interactive executable cheatsheet comparing Visual Basic and JavaScript

Visual Basic (.NET 10) JavaScript (ES2025)
Output & Running
Hello, World
The smallest complete program in each language — and the shortest distance from writing to running of anything on this anchor.
Option Strict On Imports System Module HelloWorld Sub Main() Console.WriteLine("Hello, World!") End Sub End Module
console.log("Hello, World!");
No module, no entry point, no imports, and — the part that matters most — no build step. This line runs in any browser's console, in a <script> tag, or through node file.js, exactly as written. There is nothing between the source and the running program, which is the single biggest practical difference from .NET and the reason so much of the web is written in it.
The console is more than WriteLine
The one function you will use most, and the family around it that has no .NET counterpart.
Option Strict On Imports System Imports System.Collections.Generic Module ConsoleDemo Sub Main() Dim numbers As New List(Of Integer) From {1, 2, 3} Console.WriteLine(String.Join(", ", numbers)) Console.Error.WriteLine("a problem") Dim watch = Diagnostics.Stopwatch.StartNew() watch.Stop() Console.WriteLine(watch.ElapsedMilliseconds >= 0) End Sub End Module
const numbers = [1, 2, 3]; console.log(numbers); console.log("several", "values", 42, true); console.error("a problem"); console.table([{ name: "Ada" }, { name: "Grace" }]); console.time("work"); console.timeEnd("work");
console.log prints the structure of whatever you give it — [ 1, 2, 3 ], not System.Collections.Generic.List`1 — and takes any number of arguments. The rest of the family exists because the browser's console is a real tool rather than a text stream: console.table renders an array of objects as a grid, console.time/timeEnd replaces Stopwatch, console.warn and console.error are separately filterable, and console.dir shows an object's properties.
Living Without Declarations
Nothing is declared and nothing is checked
Read all three results before deciding how you feel about this.
Option Strict On Imports System Module DeclarationDemo Function Discount(price As Decimal, percentage As Integer) As Decimal Return price * (100 - percentage) / 100 End Function Sub Main() Console.WriteLine(Discount(80D, 25)) ' Discount("eighty", 25) does not compile End Sub End Module
function discount(price, percentage) { return (price * (100 - percentage)) / 100; } console.log(discount(80, 25)); console.log(discount("80", 25)); console.log(discount("eighty", 25));
There are no parameter types, no return type and no check of any kind. discount("80", 25) happens to work, because "80" * 75 coerces to a number. discount("eighty", 25) produces NaN — not an error, not an exception, just a number that is not a number, which then flows silently into whatever you do next. This is the whole story of the page: everything Option Strict On was doing for you is now your responsibility, and the next two rows are about how the ecosystem actually copes.
Getting the checking back, without changing the language
The file stays plain JavaScript and ships unchanged — but an editor can now refuse the wrong argument.
Option Strict On Imports System Module TypedDemo ''' <summary>Applies a percentage discount.</summary> Function Discount(price As Decimal, percentage As Integer) As Decimal Return price * (100 - percentage) / 100 End Function Sub Main() Console.WriteLine(Discount(80D, 25)) End Sub End Module
/** * Applies a percentage discount. * @param {number} price * @param {number} percentage * @returns {number} */ function discount(price, percentage) { return (price * (100 - percentage)) / 100; } console.log(discount(80, 25));
A JSDoc comment carries the types the language does not. With // @ts-check at the top of the file, or checkJs in a jsconfig.json, the TypeScript compiler reads these annotations and reports errors — in your editor, at author time — while the file remains ordinary JavaScript with no build step. This is the middle path between untyped JavaScript and adopting TypeScript outright, and for a codebase that must ship source as written it is the realistic answer.
Checking a value at runtime
Since nothing is checked before the program runs, checking inside it is a skill worth having on day one.
Option Strict On Imports System Module CheckDemo Sub Describe(value As Object) If TypeOf value Is Integer Then Console.WriteLine($"integer {CInt(value) * 2}") ElseIf TypeOf value Is String Then Console.WriteLine($"string of {CStr(value).Length}") Else Console.WriteLine("something else") End If End Sub Sub Main() Describe(21) Describe("hello") Describe(3.5) End Sub End Module
function describe(value) { if (typeof value === "number" && Number.isInteger(value)) { console.log(`integer ${value * 2}`); } else if (typeof value === "string") { console.log(`string of ${value.length}`); } else if (Array.isArray(value)) { console.log("an array"); } else { console.log("something else"); } } describe(21); describe("hello"); describe(3.5); describe([1, 2]);
TypeOf x Is T becomes typeof x === "...", which knows only seven answers: "number", "string", "boolean", "undefined", "object", "function", "symbol". There is one integer type, so Integer versus Double is Number.isInteger. Two traps: typeof null is "object", a bug from 1995 that can never be fixed, and an array is also "object" — hence Array.isArray. For a class instance, instanceof.
Syntax Fundamentals
Dim becomes const, and sometimes let
Three keywords where Visual Basic has two, and one of the three should never be typed again.
Option Strict On Imports System Module BindingDemo Sub Main() Dim count As Integer = 10 Dim label As String = "widget" Const MaximumRetries As Integer = 3 count = 20 Console.WriteLine($"{count} {label} {MaximumRetries}") End Sub End Module
let count = 10; const label = "widget"; const MAXIMUM_RETRIES = 3; count = 20; console.log(count, label, MAXIMUM_RETRIES); // var is the old form — function-scoped, hoisted, avoid it var legacy = "do not use"; console.log(legacy);
const for a name you will not reassign — which is most of them — and let for one you will; both are block-scoped like Dim. Const becomes const, though it prevents rebinding rather than mutation, so a const array can still be pushed to. var is the pre-2015 form: function-scoped rather than block-scoped, and hoisted, so it exists (as undefined) from the top of the function regardless of where you wrote it. Every style guide bans it.
Blocks, semicolons and comments
Braces, parentheses and semicolons — with one formatting rule that is not merely a preference.
Option Strict On Imports System Module BlockDemo ''' <summary>Classifies a temperature.</summary> Function Classify(temperature As Integer) As String ' A line comment If temperature > 25 Then Return "Warm" ElseIf temperature > 10 Then Return "Mild" Else Return "Cold" End If End Function Sub Main() Console.WriteLine(Classify(30)) End Sub End Module
/** Classifies a temperature. */ function classify(temperature) { // A line comment /* and a block comment */ if (temperature > 25) { return "Warm"; } else if (temperature > 10) { return "Mild"; } else { return "Cold"; } } console.log(classify(30));
Every End becomes a brace, Then disappears, ElseIf becomes two words, and ' becomes //. Semicolons terminate statements, and although automatic semicolon insertion supplies most of the missing ones, it also means the opening brace must stay on the same line: a return alone on its line gets a semicolon inserted after it and returns undefined. Write the semicolons, keep the brace where it is, and let a formatter settle the rest.
Values & Coercion
One number type, and its limits
Read the second line of output — the value that came back is not the value that went in.
Option Strict On Imports System Module NumberDemo Sub Main() Dim whole As Integer = 7 Dim big As Long = 9007199254740993L Dim precise As Double = 0.1 + 0.2 Dim exact As Decimal = 0.1D + 0.2D Console.WriteLine(whole \ 2) Console.WriteLine(big) Console.WriteLine(precise) Console.WriteLine(exact) End Sub End Module
const whole = 7; const big = 9007199254740993; // silently rounded const bigSafe = 9007199254740993n; // bigint const precise = 0.1 + 0.2; console.log(Math.trunc(whole / 2)); console.log(big); console.log(bigSafe.toString()); console.log(precise); console.log(Number.MAX_SAFE_INTEGER);
There is one numeric type, a 64-bit float, and nothing else — no Integer, no Long, no Decimal. Integers are exact only to 2⁵³ (Number.MAX_SAFE_INTEGER), which is why a database id or an account number arriving as a JSON number can arrive wrong. The two answers are bigint (the n suffix, integers only, cannot be mixed with number) or keeping such values as strings. \ becomes Math.trunc(a / b), and money must not live in a number.
Values convert themselves
This is what Option Strict Off would look like if it had been the only option and nobody could turn it on.
Option Strict On Imports System Module CoercionDemo Sub Main() ' Option Strict On rejects every one of these Dim number As Integer = 1 Dim text As String = "1" Console.WriteLine(number.ToString() = text) Console.WriteLine(number + CInt(text)) Console.WriteLine("Answer: " & number) End Sub End Module
console.log(1 == "1"); console.log(1 === "1"); console.log("5" - 2); console.log("5" + 2); console.log([] + {}); console.log(1 + true); console.log(null == undefined); console.log(null === undefined);
Operators convert their operands to make the operation work, using rules almost nobody has memorized: "5" - 2 is 3 because - only means subtraction, while "5" + 2 is "52" because + also means concatenation. == converts before comparing, which is how 1 == "1" is true. Use === and !== always; the single exception worth allowing is x == null, which catches both null and undefined. Ban == in your linter and this whole family of surprises goes away.
Truthiness
The falsy list is short and complete, and the entry that is missing from it is the one that catches people.
Option Strict On Imports System Imports System.Collections.Generic Module TruthDemo Sub Main() Dim items As New List(Of String) Dim text As String = "" Dim count As Integer = 0 If items.Count = 0 Then Console.WriteLine("no items") If String.IsNullOrEmpty(text) Then Console.WriteLine("no text") If count = 0 Then Console.WriteLine("zero") End Sub End Module
const items = []; const text = ""; const count = 0; if (items.length === 0) console.log("no items"); if (!text) console.log("no text"); if (!count) console.log("zero"); if (items) console.log("an empty array is TRUTHY"); if ({}) console.log("an empty object is TRUTHY");
Exactly six values are falsy: false, 0, NaN, "", null, undefined. Everything else is truthy — including an empty array and an empty object. So if (items) tells you nothing about an array, and the emptiness test is items.length === 0. This is the opposite of Python and half the opposite of Ruby, so it is worth fixing in memory before writing much code.
Nothing becomes two things
Two absences, and knowing which one you have tells you where it came from.
Option Strict On Imports System Module NothingDemo Sub Main() Dim missing As String = Nothing Dim absentNumber As Integer? = Nothing Console.WriteLine(missing Is Nothing) Console.WriteLine(absentNumber.HasValue) Console.WriteLine(If(missing, "(unnamed)")) End Sub End Module
let neverSet; const deliberatelyEmpty = null; const person = {}; console.log(neverSet === undefined); console.log(deliberatelyEmpty === null); console.log(person.missingProperty === undefined); console.log(neverSet ?? "(unnamed)"); console.log(person.address?.city ?? "no city");
undefined means "never given a value" — an uninitialized variable, a missing object property, a function with no return. null means "deliberately empty" and only appears because someone wrote it. Both are falsy and null == undefined is true, so they are usually handled together. ?? falls back only on those two, unlike ||, which also fires on 0 and "". ?. short-circuits a whole chain instead of throwing, which is the tidiest thing JavaScript has that Visual Basic does not.
Arrays & Objects
Arrays
One growable type covers arrays, List(Of T) and the VB6 Collection — and its methods cover most of LINQ.
Option Strict On Imports System Imports System.Collections.Generic Imports System.Linq Module ArrayDemo Sub Main() Dim fruits As New List(Of String) From {"apple", "banana"} fruits.Add("cherry") Console.WriteLine(fruits.Count) Console.WriteLine(String.Join(", ", fruits)) Console.WriteLine(String.Join(", ", fruits.Where(Function(f) f.Length > 5))) End Sub End Module
const fruits = ["apple", "banana"]; fruits.push("cherry"); console.log(fruits.length); console.log(fruits.join(", ")); console.log(fruits.filter((fruit) => fruit.length > 5)); console.log(fruits.at(-1)); console.log(fruits.map((fruit) => fruit.toUpperCase()));
Addpush, Countlength, and indexing uses square brackets. The LINQ names change: Wherefilter, Selectmap, Anysome, Allevery, FirstOrDefaultfind, Aggregatereduce. Two differences worth knowing: these are eager, each building a whole new array, where LINQ is lazy; and at(-1) reaches the last element without length - 1.
Objects are the workhorse
A structure with named fields needs no class, no declaration and no dictionary — you simply write the value.
Option Strict On Imports System Imports System.Collections.Generic Module ObjectDemo Sub Main() Dim person As New Dictionary(Of String, Object) From { {"name", "Ada"}, {"age", 36} } Console.WriteLine(person("name")) person("city") = "London" For Each key As String In person.Keys Console.WriteLine(key) Next End Sub End Module
const person = { name: "Ada", age: 36 }; console.log(person.name); console.log(person["name"]); person.city = "London"; console.log(Object.keys(person)); console.log(Object.entries(person)); const { name, ...rest } = person; console.log(name, rest);
An object literal is { key: value }, and it is simultaneously what you would use a class for, what you would use a Dictionary(Of String, Object) for, and what JSON parses into. Members are reached with a dot or with brackets, added by assigning, and walked with Object.keys, Object.values or Object.entries. Destructuringconst { name, ...rest } = person — pulls parts out by name, and appears on nearly every line of modern JavaScript.
When an object is not enough: Map and Set
An object doubles as a dictionary — and the last line shows why that is not always safe.
Option Strict On Imports System Imports System.Collections.Generic Module MapDemo Sub Main() Dim ages As New Dictionary(Of String, Integer) From {{"Ada", 36}} ages("Grace") = 45 Dim seen As New HashSet(Of String) From {"cat", "dog", "cat"} Console.WriteLine(ages.Count) Console.WriteLine(ages.ContainsKey("Ada")) Console.WriteLine(seen.Count) End Sub End Module
const ages = new Map([["Ada", 36]]); ages.set("Grace", 45); const seen = new Set(["cat", "dog", "cat"]); console.log(ages.size); console.log(ages.has("Ada")); console.log(seen.size); console.log([...seen]); const plain = {}; console.log(plain.toString !== undefined); // inherited, and a real hazard
Map is the real Dictionary(Of K, V): any key type, insertion order guaranteed, a size property, and set/get/has/delete. Set is HashSet(Of T). A plain object works as a string-keyed lookup and is what JSON gives you, but it inherits members from Object.prototype, so a key called toString or constructor appears to exist when it does not. Use a Map for data with arbitrary keys; use an object for a fixed known shape.
Functions & Closures
Functions are values
No AddressOf, no Func(Of ...) type — the name of a function already is the function.
Option Strict On Imports System Imports System.Collections.Generic Module FunctionDemo Function Twice(value As Integer) As Integer Return value * 2 End Function Sub Main() Dim operations As New Dictionary(Of String, Func(Of Integer, Integer)) From { {"twice", AddressOf Twice} } Console.WriteLine(operations("twice")(21)) End Sub End Module
function twice(value) { return value * 2; } const triple = (value) => value * 3; const operations = { twice, triple }; console.log(operations.twice(21)); console.log(operations.triple(21)); console.log([1, 2, 3].map(twice));
Writing a function's name without parentheses gives you the function; adding them calls it. So a table of behaviours is just an object, and passing a function to map needs nothing around it. { twice, triple } is shorthand for { twice: twice, triple: triple }. The arrow form, (value) => value * 3, is what you write for callbacks; the function form is hoisted, so it may be called from a line above its definition.
Closures
A function keeps the variables it was created among — which is how JavaScript does private state without a class.
Option Strict On Imports System Public Class Counter Private _total As Integer = 0 Public Function Next_() As Integer _total += 1 Return _total End Function End Class Module ClosureDemo Sub Main() Dim counter As New Counter() Console.WriteLine(counter.Next_()) Console.WriteLine(counter.Next_()) End Sub End Module
function makeCounter() { let total = 0; return function next() { total += 1; return total; }; } const counter = makeCounter(); console.log(counter()); console.log(counter()); const other = makeCounter(); console.log(other());
The inner function captures total, and that captured variable lives as long as the function does. Nothing outside can reach it: this is genuine privacy, achieved with no Private keyword and no class. Each call to makeCounter creates a fresh total, which is why other() starts again at 1. Closures are how callbacks remember their context, how module state was done before modules existed, and the mechanism behind most JavaScript patterns that look surprising at first.
Optional, default and rest parameters
Defaults and varargs translate directly. The last line shows what happens when an argument is simply not passed.
Option Strict On Imports System Module ArgumentDemo Function Greet(name As String, Optional greeting As String = "Hello") As String Return $"{greeting}, {name}" End Function Function SumAll(ParamArray values() As Integer) As Integer Dim total As Integer = 0 For Each value As Integer In values total += value Next Return total End Function Sub Main() Console.WriteLine(Greet("Ada")) Console.WriteLine(Greet("Grace", "Welcome")) Console.WriteLine(SumAll(1, 2, 3)) End Sub End Module
function greet(name, greeting = "Hello") { return `${greeting}, ${name}`; } function sumAll(...values) { return values.reduce((total, value) => total + value, 0); } console.log(greet("Ada")); console.log(greet("Grace", "Welcome")); console.log(sumAll(1, 2, 3)); console.log(greet()); // no error — name is undefined
Optional x As String = "Hello" becomes greeting = "Hello", and ParamArray becomes ...values. What has no counterpart at all is arity checking: calling greet() with no arguments is legal, and name is undefined, so the result is "Hello, undefined". Passing too many is legal too. Named arguments do not exist either; the idiom is a single options object destructured in the parameter list.
Objects, Classes & Prototypes
Classes
Classes exist and read as you would expect, with real private fields and no keyword for overriding.
Option Strict On Imports System Public Class Person Private ReadOnly _name As String Public Sub New(name As String) _name = name End Sub Public Overridable Function Describe() As String Return $"I am {_name}" End Function End Class Public Class Engineer Inherits Person Public Sub New(name As String) MyBase.New(name) End Sub Public Overrides Function Describe() As String Return MyBase.Describe() & ", an engineer" End Function End Class Module ClassDemo Sub Main() Console.WriteLine(New Engineer("Ada").Describe()) End Sub End Module
class Person { #name; constructor(name) { this.#name = name; } describe() { return `I am ${this.#name}`; } } class Engineer extends Person { describe() { return super.describe() + ", an engineer"; } } console.log(new Engineer("Ada").describe());
Sub New becomes constructor, Inherits becomes extends, MyBase becomes super, and Me becomes this — required for every member access. Overridable and Overrides have no equivalent: every method can be overridden and redefining it is all it takes. A field starting with # is genuinely private, enforced at runtime. Engineer needs no constructor — omitting it inherits the parent's.
What a class actually is
The class keyword is a convenience over a mechanism that has nothing to do with types.
Option Strict On Imports System Public Class Greeter Public Function Greet() As String Return "hello" End Function End Class Module PrototypeDemo Sub Main() Dim greeter As New Greeter() ' The type is fixed at compile time Console.WriteLine(greeter.Greet()) Console.WriteLine(greeter.GetType().Name) End Sub End Module
class Greeter { greet() { return "hello"; } } const greeter = new Greeter(); console.log(greeter.greet()); // The method lives on a shared prototype object, not on the instance console.log(Object.hasOwn(greeter, "greet")); console.log(Object.getPrototypeOf(greeter) === Greeter.prototype); Greeter.prototype.shout = function () { return this.greet().toUpperCase(); }; console.log(greeter.shout());
JavaScript has no classes underneath. An object has a hidden link to another object — its prototype — and a property not found on the object is looked up there, and so on up the chain. class is syntax for building that chain. That is why adding to Greeter.prototype gives every existing instance a new method, which no .NET type can do. You will rarely write prototypes directly, but understanding them explains inheritance, this, and why old JavaScript looks the way it does.
this is decided by the call
Taking a method out of its object and calling it later is routine in callback-driven code — and it breaks.
Option Strict On Imports System Public Class Counter Private _total As Integer = 0 Public Sub Increment() _total += 1 End Sub Public ReadOnly Property Total As Integer Get Return _total End Get End Property End Class Module ThisDemo Sub Main() Dim counter As New Counter() Dim action As Action = AddressOf counter.Increment ' Me was bound when the delegate was created action() action() Console.WriteLine(counter.Total) End Sub End Module
class Counter { #total = 0; increment() { this.#total += 1; } incrementSafely = () => { this.#total += 1; }; get total() { return this.#total; } } const counter = new Counter(); const loose = counter.increment; try { loose(); } catch { console.log("a detached method lost its this"); } counter.increment.bind(counter)(); counter.incrementSafely(); console.log(counter.total);
this is set by how the function was called, not where it was written. counter.increment() sets it; pulling the method into a variable and calling that does not. AddressOf in Visual Basic binds the instance at the moment the delegate is created, so this cannot happen there. Three fixes: .bind(counter), wrapping in an arrow (() => counter.increment()), or declaring the member as an arrow property, which captures this once. Arrow functions have no this of their own — that is precisely why they are the default for callbacks.
The Event Loop
There is one thread
Read the output order: the zero-millisecond timer still runs last.
Option Strict On Imports System Imports System.Threading Module ThreadDemo Sub Main() Dim worker As New Thread(Sub() Console.WriteLine("on another thread")) worker.Start() worker.Join() Console.WriteLine("back on the main thread") End Sub End Module
console.log("first"); setTimeout(() => console.log("third — after the stack empties"), 0); Promise.resolve().then(() => console.log("second — microtask")); console.log("also first (synchronous)");
JavaScript runs on one thread with an event loop. Nothing you write interleaves with anything else — a function runs to completion before the next queued work starts, so there are no locks, no race conditions between two lines of your own code, and no Thread to start. In exchange, a long synchronous loop freezes everything, including the whole browser tab. setTimeout(..., 0) does not run immediately; it queues work for after the current stack empties, and promise callbacks (microtasks) jump ahead of it.
Task becomes Promise
The object standing for "a value that is not here yet" — with one difference in when the work starts.
Option Strict On Imports System Imports System.Threading.Tasks Module TaskDemo Async Function FetchAsync(label As String) As Task(Of String) Await Task.Delay(10) Return $"result for {label}" End Function Sub Main() Console.WriteLine(FetchAsync("one").GetAwaiter().GetResult()) End Sub End Module
function delay(milliseconds) { return new Promise((resolve) => setTimeout(resolve, milliseconds)); } function fetchValue(label) { return delay(10).then(() => `result for ${label}`); } fetchValue("one").then((value) => console.log(value));
Task(Of String) becomes Promise, and ContinueWith becomes .then(). A promise is always already running: creating one starts the work, so there is no cold task and no Start(). There is no cancellation token either — AbortController is the nearest thing and only works where the callee supports it. Because there is one thread, a promise is not a thread handle; it is a subscription to something the event loop will deliver.
Async and await
The keywords are the same words in lower case; the wrapper around them is the interesting part.
Option Strict On Imports System Imports System.Threading.Tasks Module AsyncDemo Async Function FetchAsync(label As String) As Task(Of String) Await Task.Delay(10) Return $"result for {label}" End Function Async Function RunAsync() As Task Dim both = Await Task.WhenAll(FetchAsync("one"), FetchAsync("two")) For Each value As String In both Console.WriteLine(value) Next End Function Sub Main() RunAsync().GetAwaiter().GetResult() End Sub End Module
function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function fetchValue(label) { await delay(10); return `result for ${label}`; } (async () => { const both = await Promise.all([fetchValue("one"), fetchValue("two")]); for (const value of both) { console.log(value); } })();
Async Function ... As Task(Of T) becomes async function, Await becomes await, Task.WhenAll becomes Promise.all, Task.WhenAny becomes Promise.race. await is only allowed inside an async function or at the top level of an ES module — so a plain script wraps it in an immediately-invoked async function, as above. There is no ConfigureAwait and no deadlock from blocking on a task, because there is nothing to block: the thread returns to the event loop.
JSON & Data
JSON is native
No serializer, no attributes, no type to declare — and one line of output that should worry you.
Option Strict On Imports System Imports System.Text.Json Public Class Person Public Property Name As String = "" Public Property Age As Integer End Class Module JsonDemo Sub Main() Dim person As New Person With {.Name = "Ada", .Age = 36} Dim text As String = JsonSerializer.Serialize(person) Console.WriteLine(text) Dim back = JsonSerializer.Deserialize(Of Person)(text) Console.WriteLine(back.Name) End Sub End Module
const person = { name: "Ada", age: 36 }; const text = JSON.stringify(person); console.log(text); const back = JSON.parse(text); console.log(back.name); console.log(JSON.stringify(person, null, 2)); console.log(JSON.parse('{"n": 9007199254740993}').n);
JSON is JavaScript object notation, so JSON.stringify and JSON.parse need no configuration and no target type. The third argument to stringify is the indent. Two things to watch: parse gives you a plain object with no class, no methods and no validation — you asserted nothing, so check what arrived; and a JSON number larger than 2⁵³ is silently rounded on the way in, which is why ids should travel as strings.
Dates
The built-in date type is widely considered the worst part of the language, and the month numbering is why.
Option Strict On Imports System Module DateDemo Sub Main() Dim when_ As New DateTime(2026, 8, 27) Console.WriteLine(when_.Year) Console.WriteLine(when_.Month) Console.WriteLine(when_.ToString("yyyy-MM-dd")) Console.WriteLine(when_.AddDays(5).Day) End Sub End Module
const when = new Date(2026, 7, 27); // month 7 is AUGUST console.log(when.getFullYear()); console.log(when.getMonth()); // 7, not 8 console.log(when.toISOString().slice(0, 10)); const later = new Date(when); later.setDate(later.getDate() + 5); console.log(later.getDate());
Date months are zero-based — January is 0, August is 7 — while days of the month are one-based. DateTime has none of that. There is no formatting vocabulary: ToString("yyyy-MM-dd") becomes toISOString().slice(0, 10) or Intl.DateTimeFormat. AddDays has no counterpart, so you read, add and set. Most real codebases reach for a library, and a standard replacement called Temporal is on its way; check whether it has landed before building on Date.
The Browser
The DOM replaces the forms designer
This is the part of the move no cheatsheet can shorten: the screen is a document, not a form.
Option Strict On Imports System Module FormDemo Sub Main() ' In WinForms the designer generates this, and ' controls are fields on the form class: ' Label1.Text = "Hello" ' Button1.Enabled = False Console.WriteLine("a label and a button") End Sub End Module
const label = document.createElement("p"); label.textContent = "Hello"; const button = document.createElement("button"); button.textContent = "Click me"; button.disabled = true; document.body.append(label, button); const found = document.querySelector("p"); console.log(found.textContent);
There is no designer and no control class. A page is a tree of elements — the DOM — and JavaScript creates, finds and changes nodes in it. Label1.Text becomes element.textContent, Button1.Enabled = False becomes button.disabled = true, and querySelector finds elements with the same selectors CSS uses. Layout is CSS, not anchors and docking. Learning HTML and CSS is most of the real work of this move, and it is worth budgeting for honestly.
Events
The same publish-and-subscribe idea, with the event named by a string rather than declared.
Option Strict On Imports System Public Class Uploader Public Event Finished As EventHandler Public Sub Run() RaiseEvent Finished(Me, EventArgs.Empty) End Sub End Class Module EventDemo Sub Main() Dim uploader As New Uploader() AddHandler uploader.Finished, Sub(sender, args) Console.WriteLine("done") uploader.Run() End Sub End Module
class Uploader extends EventTarget { run() { this.dispatchEvent(new CustomEvent("finished", { detail: { count: 1 } })); } } const uploader = new Uploader(); uploader.addEventListener("finished", (event) => { console.log("done", event.detail.count); }); uploader.run();
Public Event Finished has no declaration equivalent — an event is identified by a string name, which means a typo in addEventListener("finihsed", ...) silently subscribes to nothing. AddHandler becomes addEventListener, RemoveHandler becomes removeEventListener, and RaiseEvent becomes dispatchEvent. Extra data travels in event.detail rather than in a custom EventArgs subclass. In the browser every element is an EventTarget, so a click handler is the same mechanism.
Calling a web service
HttpClient becomes a built-in function, with one behaviour worth memorizing before you rely on it.
Option Strict On Imports System Imports System.Net.Http Imports System.Threading.Tasks Module HttpDemo Async Function GetAsync(url As String) As Task(Of Integer) Using client As New HttpClient() Dim response = Await client.GetAsync(url) Return CInt(response.StatusCode) End Using End Function Sub Main() ' Would call a real service Console.WriteLine("HttpClient") End Sub End Module
async function getStatus(url) { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.json(); } console.log(typeof fetch); console.log(typeof getStatus);
fetch is built into browsers and Node — nothing to construct, nothing to dispose. The trap: a 404 or a 500 does not reject the promise. fetch only rejects on a network failure, so await fetch(...) succeeds for an error response and you must check response.ok yourself. The body arrives separately and asynchronously — await response.json() or .text() — because the headers land before the body does.
Modules, npm & Tooling
Imports becomes import — twice over
A file is a module and its path is its name — but there are two module systems in the wild, and telling them apart is a daily chore.
Option Strict On Imports System Namespace Geometry Public Module Area Public Function Rectangle(width As Double, height As Double) As Double Return width * height End Function End Module End Namespace Module ImportDemo Sub Main() Console.WriteLine(Geometry.Area.Rectangle(3, 4)) End Sub End Module
// area.js (ESM — the standard) export function rectangle(width, height) { return width * height; } // main.js import { rectangle } from "./area.js"; console.log(rectangle(3, 4)); // The older Node format, still everywhere: // const { rectangle } = require("./area.js"); // module.exports = { rectangle };
Anything exported is visible elsewhere; everything else is private to the file. There is no namespace declaration and no project-level import. The complication with no .NET parallel is that two module systems coexist: ESM (import/export, the standard, what browsers use) and CommonJS (require, Node's original). Which one a file gets is decided by its extension and by "type" in package.json, and interop between them is the most tedious part of Node work.
NuGet becomes npm
The packaging story maps across piece by piece, with one difference in scale that is worth knowing before you start.
Option Strict On Imports System Imports System.Collections.Generic Module PackageDemo Sub Main() ' A .vbproj lists PackageReference entries; ' NuGet restores them into the build Dim story As New Dictionary(Of String, String) From { {"manifest", ".vbproj"}, {"registry", "NuGet"}, {"restore", "dotnet restore"} } For Each entry In story Console.WriteLine($"{entry.Key}: {entry.Value}") Next End Sub End Module
const story = { manifest: "package.json", registry: "npm", install: "npm install", lockfile: "package-lock.json", scripts: "npm run build", }; for (const [key, value] of Object.entries(story)) { console.log(`${key}: ${value}`); }
package.json is the .vbproj, npm is nuget.org, npm install is dotnet restore, and package-lock.json pins exact versions. package.json also holds scripts, which is where build, test and start commands live — there is no MSBuild. The difference in scale: npm packages are small and numerous, dependencies nest, and a modest project routinely pulls in hundreds. That is normal, and it is also why npm audit and lockfile review matter more here than they do with NuGet.
Error Handling
Try/Catch, with one catch block
The construct keeps its shape; the branching moves inside a single handler.
Option Strict On Imports System Module TryDemo Sub Main() Try Dim parsed = JsonBad() Console.WriteLine(parsed) Catch error_ As FormatException Console.WriteLine($"Bad format: {error_.Message}") Catch error_ As Exception Console.WriteLine($"Something else: {error_.Message}") Finally Console.WriteLine("always runs") End Try End Sub Function JsonBad() As Integer Return Integer.Parse("not a number") End Function End Module
try { JSON.parse("{ not json }"); } catch (error) { if (error instanceof SyntaxError) { console.log(`Bad JSON: ${error.message}`); } else { console.log(`Something else: ${error.message}`); } } finally { console.log("always runs"); }
There is one catch and it takes no type, so several typed Catch clauses become a chain of instanceof tests. There is no exception filter, so When becomes an if plus a rethrow. And anything at all may be thrown — a string, a number, an object — so a defensive handler checks error instanceof Error before reading .message. The catch binding may be omitted entirely (catch { }) when you do not need it.
Throwing your own error
The inheritance you already saw, applied to Error — plus one line everybody forgets.
Option Strict On Imports System Public Class InsufficientFundsException Inherits Exception Public ReadOnly Property Shortfall As Decimal Public Sub New(shortfall As Decimal) MyBase.New($"Short by {shortfall}") Me.Shortfall = shortfall End Sub End Class Module ThrowDemo Sub Main() Try Throw New InsufficientFundsException(25D) Catch error_ As InsufficientFundsException Console.WriteLine($"{error_.Message} (short {error_.Shortfall})") End Try End Sub End Module
class InsufficientFundsError extends Error { constructor(shortfall) { super(`Short by ${shortfall}`); this.name = "InsufficientFundsError"; this.shortfall = shortfall; } } try { throw new InsufficientFundsError(25); } catch (error) { if (error instanceof InsufficientFundsError) { console.log(`${error.message} (short ${error.shortfall})`); } }
Inherits Exception becomes extends Error, MyBase.New(message) becomes super(message). The line with no .NET counterpart is this.name = "...": without it the error prints as a plain Error in every log and stack trace, because name is an ordinary property rather than something derived from the class. Convention ends the class name in Error, not Exception.
⚠ Gotchas for Visual Basic Programmers
⚠ A wrong value keeps going
The most expensive habit to unlearn: a bad value does not stop, it spreads.
Option Strict On Imports System Module SilentGotcha Sub Main() Dim quantities() As String = {"3", "4", "oops"} Dim total As Integer = 0 For Each text As String In quantities Dim value As Integer If Integer.TryParse(text, value) Then total += value Else Console.WriteLine($"skipping {text}") End If Next Console.WriteLine(total) End Sub End Module
const quantities = ["3", "4", "oops"]; let total = 0; for (const text of quantities) { total += Number(text); } console.log(total); // NaN, and nothing said so let checked = 0; for (const text of quantities) { const value = Number(text); if (Number.isNaN(value)) { console.log(`skipping ${text}`); continue; } checked += value; } console.log(checked);
Number("oops") is NaN — a number that is not a number — and every arithmetic operation involving it produces NaN, silently, all the way to whatever the user finally sees. There is no exception and no TryParse. Worse, NaN !== NaN, so the only test is Number.isNaN(x). Check at the point of conversion, every time; that is the discipline replacing Option Strict On, and nothing in the language will remind you.
⚠ Sort compares numbers as text
Two bugs in one method, and the first of them produces a plausible-looking wrong answer.
Option Strict On Imports System Imports System.Collections.Generic Module SortGotcha Sub Main() Dim numbers As New List(Of Integer) From {10, 9, 100, 1} numbers.Sort() Console.WriteLine(String.Join(", ", numbers)) End Sub End Module
const numbers = [10, 9, 100, 1]; console.log([...numbers].sort()); console.log([...numbers].sort((a, b) => a - b)); console.log(numbers); numbers.sort(); console.log(numbers); // the original changed
sort() with no comparator converts every element to a string and sorts lexicographically, so 9 lands after 100. Always pass one: (a, b) => a - b for numbers, (a, b) => a.localeCompare(b) for text. The second problem is that it sorts in place and returns the same array, unlike LINQ's OrderBy — which is why the copies above use [...numbers]. toSorted() is the newer non-mutating version.
⚠ Two identical objects are not equal
There is no value type, so every object comparison asks about identity.
Option Strict On Imports System Public Structure Point Public ReadOnly X As Integer Public ReadOnly Y As Integer Public Sub New(x As Integer, y As Integer) Me.X = x Me.Y = y End Sub End Structure Module EqualityGotcha Sub Main() Dim left As New Point(1, 2) Dim right As New Point(1, 2) Console.WriteLine(left.Equals(right)) End Sub End Module
const left = { x: 1, y: 2 }; const right = { x: 1, y: 2 }; console.log(left === right); console.log(left === left); console.log([1, 2] === [1, 2]); console.log(JSON.stringify(left) === JSON.stringify(right));
=== on objects and arrays compares identity, never contents. There is no Structure, no record, and no Equals to override that === would consult. Comparing by value means writing it out field by field, using a library's deep-equal, or comparing a canonical string — and the JSON.stringify trick works only when key order matches and the values are JSON-safe, so treat it as a debugging aid rather than a technique.
⚠ const does not mean unchangeable
A const array can be emptied, refilled and reordered — what it cannot do is become a different array.
Option Strict On Imports System Imports System.Collections.Generic Module ConstGotcha Sub Main() Dim original As New List(Of Integer) From {1, 2, 3} Dim alias_ As List(Of Integer) = original Dim copy As New List(Of Integer)(original) alias_.Add(4) Console.WriteLine(original.Count) Console.WriteLine(copy.Count) End Sub End Module
const original = [1, 2, 3]; const aliased = original; const copy = [...original]; aliased.push(4); // original = []; // THIS would be an error console.log(original.length); console.log(copy.length); const frozen = Object.freeze({ a: 1 }); frozen.a = 2; console.log(frozen.a); // still 1, silently
const prevents rebinding, not mutation: original = [] is an error, original.push(4) is not. Assignment shares the object, so aliased is the same array under another name; copying is [...original] or original.slice(), both shallow. Object.freeze makes an object genuinely immutable, but only one level deep and — in non-strict code — it fails silently rather than throwing, as the last line shows.
⚠ No My namespace, and two different runtimes
The bigger adjustment is not what is missing — it is that the same language runs in two places with different capabilities.
Option Strict On Imports System Module PlatformGotcha Sub Main() Console.WriteLine(Environment.MachineName.Length > 0) Console.WriteLine(IsNumeric("42")) Console.WriteLine(Now.Year > 2000) End Sub End Module
console.log(!Number.isNaN(Number("42"))); console.log(new Date().getFullYear() > 2000); // Machine name needs Node, and does not exist in a browser: // const os = require("node:os"); // console.log(os.hostname()); console.log(typeof globalThis);
IsNumeric becomes Number() plus Number.isNaN; Now becomes new Date(). My.Computer and My.Computer.FileSystem exist only in Node (node:os, node:fs) and simply are not there in a browser, where document and window are instead. Code meant for both must avoid assuming either. MsgBox and InputBox have no real counterpart — the browser's alert and prompt exist but block the whole page and no production application uses them.