
There are, broadly, two kinds of people who begin with JavaScript. The first kind arrive wide-eyed. They run console.log("hello world"), feel briefly like they have accessed something important, learn the syntax, pick up some networking concepts, develop a working sense of how the web functions. Then, gradually, the picture complicates. They wrestle with Node version conflicts, competing package managers, bundlers, transpilers, polyfills, and frameworks constructed upon frameworks. One day they wake up and find that no matter how deeply they dig into the language, there is always some behaviour at the bottom that does not follow from anything they have learned. The language, at its foundations, is irregular.
The second kind know this already and stay anyway, out of familiarity or employment or inertia.
JavaScript as a language is not fine. TypeScript improves the surface considerably, but the surface is not where the problem lives. My specific grievance — the one that wore me down most persistently — is this. Two function forms, one language:
function f() { ... }
() => { ... }
They behave differently with respect to this. Regular functions receive this at call time:
obj.f() → this = obj
f() → this = global / undefined
Arrow functions carry no this of their own. They inherit it from the enclosing lexical scope at the time of their creation.
The result is a binding that is neither consistently lexical nor consistently dynamic — it is context-dependent in a way that cannot be predicted from any single rule. It behaves like an invisible implicit argument that changes shape depending on how the function was called, where it was defined, and what the surrounding code happens to be doing.
In C, you pass a pointer or a reference. You can see it. You know what it refers to. The relationship is explicit.
The Culture Around JavaScript
The language's irregularities might be tolerable if the culture surrounding it were not so committed to celebrating them. There exists a certain species of JavaScript practitioner who takes genuine pride in being able to predict the output of:
console.log(1 + 2 + "3");
This is not engineering knowledge. It is the memorisation of corner cases in a language that contains too many of them. That these questions persist in technical interviews — displacing genuinely interesting topics like asynchronous I/O patterns, debouncing, or event loop mechanics — is a reasonable measure of how much of the ecosystem is built on the wrong foundations.
async/await looks clean. Beneath it sits a microtask queue, a macrotask queue, an event loop, Promise state machines, resolution chains, and error propagation that behaves differently from synchronous code in ways that are not obvious until they cause a problem.
Then there is the configuration surface of a production JavaScript application:
- A package manager
- A bundler
- A linter
- A formatter
- A test runner
- A framework
- Configuration files to configure the files that configure the files
Each of these is a decision that must be made before a single line of actual product logic is written.
What Runs Beneath
The thing that troubled me most, ultimately, was this. In C, the path from source to execution is legible:
source → compiler → assembly → machine code
In JavaScript:
source → engine (V8, SpiderMonkey, etc.) → parse → AST → interpret → JIT compile → optimise → deoptimise → reoptimise → ...
None of this is visible. Memory layout — hidden. Stack and heap behaviour — largely hidden. Object representation — engine-dependent and subject to change between versions.
You are writing code whose performance characteristics you cannot directly reason about, because the machine executing it is not the machine you are writing for.
A Change of Direction
I am now learning C. Manual memory management. Pointers. Segmentation faults that announce themselves loudly and honestly the moment you make an error. Nothing is hidden.
You allocate memory — you own it. You free memory — or you leak it and accept the consequences. You pass a pointer — you know exactly where it points and what will happen when it is dereferenced.
Alongside this, I am working through computer organisation and architecture, and the theory of computation — automata, formal grammars, the boundary between what is computable and what is not.
These are the prerequisites to what I actually intend to build: a compiler. From source. No LLVM. A language specification, a lexer, a parser, an AST, semantic analysis, code generation, and whatever suffering those steps require.
Not because it is the efficient path. Because I want to understand — fully, at every level — how programming languages actually work. JavaScript never offered that understanding. It offered abstraction in place of it.
JavaScript taught me how to start. C is teaching me how things actually function. Somewhere in the space between all three, I intend to build a language of my own — one whose design decisions I can defend, and which does not require its users to memorise a catalogue of exceptions in order to use it correctly.
I have rarely been more interested in the work ahead.