I Spent 2 Weeks Adding Go’s Defer to TypeScript. Here’s What Happened.

You know that sinking feeling when you’re deep in a function, juggling file handles, database connections, and temporary files, and you realize you’ve forgotten to close something? That moment of panic when you mentally trace every possible return path, trying to remember if you slapped a finally block on the right spot. I’ve been there. You’ve been there. We’ve all been there.

But here’s the thing: every time you reach for try/finally, you’re admitting defeat. You’re saying, “I don’t trust myself to remember where this cleanup goes, so I’ll wrap everything in a verbose block and hope the control flow doesn’t get too clever.” It works, but it’s a hack. A reliable hack, but a hack nonetheless.

That’s why I decided to do something about it. I spent two weeks hacking the TypeScript compiler to add Go’s defer statement. And what I found changed how I think about language features entirely.

Go’s defer is elegant: you declare cleanup right next to the resource allocation. It’s declarative, not imperative. You say, “when this function exits, run this code,” and the compiler figures out the rest. No nesting, no mental stack traces. Just clean, linear code.

But adding it to TypeScript wasn’t just about syntax. It was about shifting the burden of correctness from your brain to the compiler’s AST. The compiler now has to understand control flow well enough to know where to insert the deferred call—before every return, after every throw, at the end of every branch. That’s a non-trivial transformation.

I started by studying the TypeScript parser and emitter. The parser was easy—add a new token, a new AST node type. The real challenge was in the emitter: how do you transform the AST so that defer behaves like try/finally without breaking existing semantics? I ended up creating a pre-emission pass that wraps each function body in a synthetic try block and inserts the deferred calls into the finally clause. It’s a brute-force approach, but it works.

And here’s the wild part: the debate between defer and try/finally isn’t about syntax at all. It’s about trust. Do you trust yourself to never forget a cleanup? Or do you trust the compiler to handle it for you? The Go community chose the compiler. The TypeScript community has historically chosen the developer. But after this experiment, I’m convinced the compiler is the better bet.

I ran the modified compiler against a handful of real-world TypeScript projects. The results were instructive: in every case where try/finally was used, the developer had to manually ensure that the finally block was reachable. With defer, that guarantee is baked into the language. I saw one developer’s face light up when he realized he could replace a 15-line nested try/catch/finally with a single defer and a straightforward linear flow.

If you’re a language designer, take note: the most impactful features are the ones that reduce cognitive load, not the ones that add more syntax. Defer doesn’t give you new power—it gives you peace of mind. And that’s what makes a language addictive.

So next time you’re staring at a finally block, ask yourself: are you writing a hack, or are you writing a feature? Maybe it’s time to borrow a little wisdom from Go.

FAQ

Q: Isn't try/finally more predictable because it's explicit?

A: Explicit doesn't mean predictable. Try/finally forces you to trace every exit path manually. Defer is declarative: the compiler guarantees the cleanup will run, regardless of control flow complexity. It's actually more predictable because it removes the human error factor.

Q: What's the practical implication for a TypeScript developer?

A: If this feature were added to TypeScript, you'd write cleaner, safer code. Resource acquisition and cleanup would be paired right next to each other, reducing the chance of forgotten closes or leaks. It's a small syntax change that significantly improves code quality.

Q: Isn't this just a language fad? Couldn't we achieve the same thing with better patterns?

A: You could, but patterns are only as good as the discipline to enforce them. Defer makes the correct behavior the default, not the exception. It's not a fad—it's a fundamental shift from imperative to declarative resource management, and it's been proven in Go and Zig.

📎 Source: View Source