VB.NET has a reputation problem. Ask most developers about it and you'll hear the same things: it's verbose, it's enterprise-only, it's the language your company makes you use, not the one you'd choose. And I get it — if your mental model of VB.NET is a thousand-line Module full of ReDim Preserve and hand-rolled string concatenation, then yes, that's not a good time.
But I think the reputation is misplaced. Verbosity isn't a property of the language. It's a property of the code you write on top of it.
Here's an HTTP request in my own Fetch library — a thin wrapper I built over .NET's HttpWebRequest, inspired partly by JavaScript's fetch API:
Dim data As JToken = (New Request("http://example.com") With {
.Authorization = Authorization.Basic(username, password),
.Accept = "application/json"
}).Fetch().IfOk().Expect("application/json").Json()
One expression. It constructs the request, sets credentials, fires it, asserts success, checks the content type, and parses JSON. That's as terse as Python's requests or JavaScript's fetch — and it reads clearly from left to right.
And everything here is optional with sensible defaults. Method defaults to GET. Content-Type is inferred from whatever you put in Body — pass a JObject and the library knows it's JSON without being told. Query string parameters embedded in the URL are automatically parsed and decoded. All headers are equal and can be set via the .Headers array, but the ones you actually reach for — Authorization, Accept — are promoted to dedicated properties for convenience.
Those defaults feel natural to me, which is kind of the beauty of having written the library myself. They reflect how I think about HTTP requests, not how a committee decided a general-purpose API should work.
If you don't like long one-liners, the same library works just as well broken into steps:
Dim r As Response = Await (New Request("http://example.com")).FetchAsync()
If r.Ok Then
Dim data As JToken = Await r.JsonAsync()
Else
Dim errorResponse As String = Await r.TextAsync()
' Log non-2xx status response
End If
Same library. Same types. You choose the shape that fits the situation.
Here's another example from my web framework — a content-negotiated error handler:
<HttpRoute.Exception(HttpStatusCode.Not_Found)>
Public Function ResourceNotFound() As Negotiated
Return New Negotiated From {
{"text/html", Function() New HtmlTemplateResponse(Templates("notfound.html"), HttpStatusCode.Not_Found)},
{"application/json", Function() New JsonResponse(New JObject From {{"error", "Resource not found"}}, HttpStatusCode.Not_Found)}
}
End Function
This is declarative code. It says: here are the content types I handle and what to return for each. There's no parsing of Accept headers, no quality factor sorting, no fallback logic. Just a map of types to responses.
The imperative work lives elsewhere, in the Negotiated class itself:
Public Function Resolve(e As RequestEventArgs) As Task(Of HttpResponse)
Dim directives As List(Of Directive) = If(e.RequestHeader("Accept"), "").
Split(","c).
ToList.
ConvertAll(AddressOf Directive.Parse).
FindAll(Function(x) x.Valid)
If directives.Count = 0 Then directives.Add(Directive.Parse("*/*"))
directives.Sort(Function(a, b) b.Qualifier.CompareTo(a.Qualifier))
For Each directive As Directive In directives
For Each key As (MediaType As String, SubType As String) In Handlers.Keys
If directive.Matches(key) Then
Dim handler As [Delegate] = Handlers(key)
If TypeOf handler Is Func(Of Task(Of HttpResponse)) Then
Return DirectCast(handler, Func(Of Task(Of HttpResponse))).Invoke()
Else
Return Task.FromResult(DirectCast(handler, Func(Of HttpResponse)).Invoke())
End If
End If
Next
Next
Throw New NotAcceptableException
End Function
This is the code that makes the declaration above work. It parses the Accept header, sorts by quality factor, walks the registered handlers, and picks the best match. It's substantially more complex and more imperative — lambdas, type checking, sorting, iteration.
But that's the point. The complexity doesn't disappear. It migrates downward into infrastructure code that gets written once and tested once. The application layer — the code you write every day, the code that a new developer reads to understand what the system does — stays thin and declarative.
Here's a call from my configuration system that saves a JSON file to disk:
Core.File.Write(Path, MakeBackup:=BackupOnSave, Sub(x) ToJson(SerializationOptions.WithSecrets).WriteTo(x))
The caller says what to write — serialize this object as JSON and write it to the stream. Everything else is handled by File.Write:
Public Shared Sub Write(Path As FilePath, MakeBackup As Boolean, Write As Action(Of IO.FileStream))
Dim alreadyExists As Boolean = IO.File.Exists(Path)
Dim writeTo As FilePath = If(alreadyExists, TempPath(Path), Path)
Using fs As New IO.FileStream(writeTo, IO.FileMode.Create, IO.FileAccess.Write, IO.FileShare.Read)
Write(fs)
End Using
If alreadyExists Then IO.File.Replace(writeTo, Path, If(MakeBackup, CStr(BackupPath(Path)), Nothing))
End Sub
The temp file, the atomic replace via IO.File.Replace, the conditional backup — all of that is invisible to the caller. The Action(Of IO.FileStream) parameter is the seam between what gets written and how it gets written safely. That's separation of concerns without an interface, without an abstract class, without dependency injection. Just a lambda.
And here's the thing: I wrote VB.NET for many years before I started writing code like this. Func(Of) and Action(Of) were introduced into the language but not really advertised, so I kept writing throwaway Delegate classes. But when I did learn about them, something clicked immediately because I've been writing closures in JS for a long time — passing functions around as arguments, building APIs where behavior is a parameter — rewired how I thought about structuring code.
That influence runs in both directions and across multiple languages. The <HttpRoute.Exception(HttpStatusCode.Not_Found)> attribute on the error handler above? That's a pattern I borrowed from Python's FastAPI, where decorators like @app.exception_handler(404) attach behavior to functions declaratively. The route syntax <HttpRoute.Get("/article/int:id")> draws from the same well — Flask and FastAPI both use that style of inline type constraints in URL patterns.
VB.NET's custom attributes turn out to be a natural fit for this kind of declarative routing. The syntax is different but the idea is the same: metadata on a method that a framework reads at registration time. I did set out to bring the FastAPI mindset to VB.NET — not a line-for-line clone, but the same philosophy of declarative routing with minimal ceremony. And VB.NET turned out to be a willing participant. Take a route handler that declares its return type As JsonResponse but actually returns New JObject From {{"error", "Resource not found"}}. That works because I wrote a widening CType operator from JContainer (the base class for JObject) to JsonResponse, and the compiler applies it silently. The return type in the method signature serves as both documentation and infrastructure — it tells the reader what kind of response this is, and it tells the framework how to serialize it, without the method body needing to say so explicitly.
Programming languages aren't islands. The patterns that work well tend to migrate, and even just exposure to other languages lets you recognize when a technique from one world solves a problem in another. VB.NET's lambda support, combined with its attribute system and collection initializers, means it can absorb ideas from Python and JavaScript without fighting the syntax.
When people call VB.NET verbose, what they usually mean is that the code they've seen in VB.NET is verbose. And that's often true — but it's true of most languages when you're working directly against low-level framework APIs without any abstraction over them.
The expressiveness of your application code is largely a function of the libraries and abstractions you build beneath it. VB.NET gives you object initializers, collection initializers, lambda expressions, extension methods, operator overloading — all the tools you need to create APIs that read cleanly and say only what needs to be said.
The question isn't whether the language is capable of concise, expressive code. It is. The question is whether someone, or perhaps yourself, has done the upfront work of building the abstractions that make it possible.