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 Moduleconsole.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 Moduleconst 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 Modulefunction 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 Modulefunction 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 Modulelet 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 Moduleconst 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 Moduleconsole.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 Moduleconst 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 Modulelet 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 Moduleconst 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()));Add→push, Count→length, and indexing uses square brackets. The LINQ names change: Where→filter, Select→map, Any→some, All→every, FirstOrDefault→find, Aggregate→reduce. 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 Moduleconst 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. Destructuring — const { 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 Moduleconst 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 hazardMap 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 Modulefunction 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 Modulefunction 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 Modulefunction 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 undefinedOptional 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 Moduleclass 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 Moduleclass 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 Moduleclass 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 Moduleconsole.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 Modulefunction 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 Modulefunction 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 Moduleconst 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 Moduleconst 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 Moduleconst 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 Moduleclass 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 Moduleasync 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 Moduleconst 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 Moduletry {
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 Moduleclass 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 Moduleconst 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 Moduleconst 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 changedsort() 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 Moduleconst 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 Moduleconst 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, silentlyconst 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 Moduleconsole.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.