There's a specific kind of confusion that doesn't show up in tutorials. It's not "I don't know the syntax." You can look up syntax. It's more like... you're using a library or a framework, things are working, and you still have this vague feeling that you don't really know what's going on.
I've been there more than I'd like to admit. I used React before I actually understood JavaScript. I used Go's context package while having basically no mental model of what context even was. I was driving the car with no idea what was under the hood. And it showed. Not always immediately, but the moment something broke in an unexpected way, or I tried to pick up something new, the gaps were obvious.
But I've been trying something lately that's been genuinely helping, and it’s been genuinely helping. I'm calling it the two-track method, though honestly it's less of a structured method and more of a habit I've been building. The idea is simple: before I reach for the abstraction, I build a tiny version of it myself first.
The Method
It doesn't have to be complete. Doesn't have to be good. Just functional enough that I've actually felt the problem the abstraction is solving.
That's the thing I keep coming back to. Abstractions are solutions to problems. But if I've never felt the problem, the solution feels arbitrary. I end up memorizing API surfaces instead of understanding them, and that only gets me so far.
So the two tracks look like this:
Track 1: Build the primitive myself. Keep it small. Get it working.
Track 2: Pick up the real library. See what's different.
I used to jump straight to Track 2. Spend weeks getting comfortable with a library, never really shake that feeling of depending on it without understanding it. Flipping the order, even just once, has made a noticeable difference for me. Something clicks that's hard to un-click.
Example 1: HTTP from scratch To net/http
A while back I implemented the HTTP protocol from scratch in Go. Not as a joke, as an actual exercise. I was parsing raw bytes off a TCP connection, pulling apart request lines, validating methods, checking HTTP versions. Stuff like this:
func parseRequestLine(b []byte) (*RequestLine, int, error) {
idx := bytes.Index(b, SEPARATOR)
if idx == -1 {
return nil, 0, nil
}
startLine := b[:idx]
parts := bytes.Split(startLine, []byte(" "))
if len(parts) != 3 {
return nil, 0, fmt.Errorf("invalid request line: %s; want 3 parts, have %d", startLine, len(parts))
}
// ...
}
It was tedious. Slightly painful. I had to actually think about what an HTTP request is made of at the byte level. Method, target, version, headers, body. All of it sitting in a raw buffer, waiting to be parsed.
So then when I picked up net/http, things that would have just been "okay I guess I just do it this way" suddenly had reasons behind them. What a Handler is actually doing. Why ResponseWriter is an interface. What's happening under the hood when a request comes in and gets routed somewhere. I wasn't guessing anymore. I had a mental model.
The part I didn't expect though: the knowledge transferred. Understanding HTTP at that level started showing up in other places. Writing command parsers. Understanding how the browser handles HTML. Things that look unrelated on the surface but share the same underlying ideas. Once you've seen the raw thing, you start recognizing its shape everywhere.
Example 2: Tiny context library To Go's context package
Context was one of those things that just wouldn't click for me. I'd see ctx context.Context in every function signature, pass it along like I was supposed to, and move on. I knew it had something to do with cancellation and deadlines. That was about it.
So I built a tiny version of it myself. And I mean tiny. A struct with a done channel, an error, a parent pointer, and a key-value pair. Then WithCancel, WithTimeout, WithValue. A goroutine watching for parent cancellation and propagating it down.
type MyContext struct {
done chan struct{}
err error
parent *MyContext
key, val any
}
func WithCancel(parent *MyContext) (*MyContext, func()) {
ctx := &MyContext{
done: make(chan struct{}),
parent: parent,
}
go func() {
if parent == nil {
return
}
select {
case <-parent.Done():
ctx.cancel(fmt.Errorf("parent cancelled"))
case <-ctx.Done():
return
}
}()
cancel := func() {
ctx.cancel(fmt.Errorf("manually cancelled"))
}
return ctx, cancel
}
Then I wired up a little worker that listened on the done channel:
func worker(ctx *MyContext, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d stopping: %v\n", id, ctx.Err())
return
default:
fmt.Printf("Worker %d working...\n", id)
time.Sleep(500 * time.Millisecond)
}
}
}
And let me tell you, this was a good idea. ctx context.Context started making a lot more sense. It's just a done channel, an error, and a chain of parents.
Example 3: Vanilla JS state management To React
This one was a big problem. I jumped into React pretty early. Like, embarrassingly early. I'd done a JavaScript tutorial, felt reasonably okay about it, and went straight into a React course. And I got through it. I could build things.
But I couldn't tell you what was React and what was just JavaScript. It was all one blur. When I'd write JSX I genuinely wasn't sure sometimes which bits were React magic and which bits were plain JS. That kind of confusion has a cost. It made me nervous about touching things I hadn't seen in a tutorial before. And it made me feel like switching to another framework would basically mean starting from scratch.
But lately I’ve been writing a lot of vanilla JavaScript. And at some point I’ve had to manually manage state for a small project. No libraries. Just me, some variables, and a lot of manual DOM updates whenever something changed. Not gonna lie, it hasn’t been particularly fun. But it’s been immensely useful.
React is starting to make a lot more sense now. Not just what it does, but why it exists. The problem it's solving isn't abstract anymore, I'm really feeling it. The state management, the re-rendering, the component model… all of it is suddenly clicking into place.
And the confidence thing is real. I feel like I could pick up Vue or Svelte or whatever comes next without too much trouble. Because I now understand what they're all fundamentally trying to solve.
It's Not Just Libraries
I want to mention something that's been running alongside all of this, because I think it's the same idea wearing different clothes.
A lot of the clarity I've been getting hasn't just come from building primitives of tools. It's come from learning the concepts underneath the concepts. The stuff that lives below the library layer entirely.
Take networking. I spent some time learning about the OSI model, how data actually moves across a network, what's happening at each layer. Dry stuff on the surface. But when I came back to HTTP after that, things that were previously just "that's how it works" had actual reasons behind them. The layers aren't arbitrary. TCP being reliable while UDP isn't is more than a trivia fact. It's a design decision that makes sense once you actually understand what problem each one is solving. That context made everything above it stickier.
Or memory. I did a little C specifically to understand how memory works. Pointers, the stack, the heap, manually allocating and freeing things. It was pretty weird to be honest. C will just let you access garbage memory.
Anyway though, when I came back to Go after that, I understood more deeply why slices behave the way they do. What the garbage collector is actually relieving you of. How goroutines sit in memory differently from OS threads.
Same with operating systems. I'm still pretty early on this one. But even a surface-level understanding of how an OS manages processes and schedules work has given me a better mental model for writing concurrent code. It's like the floor got more solid.
I think the pattern is the same one. Abstractions make more sense when you understand what they're abstracting. Whether that's a library abstracting over a protocol, or a language abstracting over memory management, or a runtime abstracting over the operating system. The further down you've looked, even just once, even just a little, the less arbitrary everything above it feels.
I'm not saying go learn C and then the Linux kernel before you write another line of JavaScript. That's not what this is. It's more that I've found it worth occasionally asking: what is this thing actually sitting on top of? And then going and poking at that layer, even briefly. It pays off in ways that are hard to predict in advance.
Conclusion
I'm still in the middle of all this, to be clear. There's a long list of things I haven't built from scratch yet and probably won't for a while. I'm not trying to re-implement the entire stack before I'm allowed to use a library.
But the ones I have done? I can feel the difference. It's hard to describe precisely, but it's somewhere between confidence and calm. Not "I know everything about this tool" confidence. More like "I'm not just hoping this works" confidence. There's a solidity to it.
I think that's what this method is really about, underneath everything. It's not about being the person who built a router from scratch. It's about not feeling like you're guessing. Because there's a real difference between using a tool and understanding a tool. One of them means you're dependent on things staying familiar. The other means you can move around, adapt, pick things up, put things down.
I stumbled into this backwards, through confusion and frustration more than any deliberate plan. The HTTP implementation wasn't a grand learning strategy, it was an exercise that turned out to matter more than I expected. Same with context, same with vanilla JS state. I just kept noticing that the things I'd felt the pain of first were the things that stuck.
So that's what I'm doing now. Trying to feel the problem before I reach for the solution. It's slower in the short term. But I'm finding it's a lot faster than going back to fill in gaps later.
Top comments (0)