Adding Go's Defer to the TypeScript Compiler

Posted by healeycodes 17 hours ago

Counter20Comment25OpenOriginal

Comments

Comment by throwaway81523 16 hours ago

Article describes try/finally as a hack to get the effect of defer, but it looks to me like it's the other way around? Try/finally is more traditional, in one form or another.

Comment by Rohansi 15 hours ago

It's also necessary here because TypeScript has exceptions and you'd expect your `defer`s to execute even when an exception is thrown.

Comment by kaashif 15 hours ago

If the idea of computers is that they remember stuff for you and do stuff for you, then both try/finally and defer seem like hacks to work around not having RAII like e.g. Rust or C++ where resources are closed/disposed of/deallocated automatically.

Try X finally dispose of all resources. X, defer clean up X. Or how about just X and cleanup is done automatically, with the author of the resources deciding what is needed to clean X up.

Comment by mcintyre1994 15 hours ago

Comment by kaashif 14 hours ago

What if you forget to use that and just use let or const? It is better than plain try-finally or defer because you don't have to remember how to dispose of the resources but in terms of remember to dispose of the resource at all, I don't think that is all that different from Python's with or Java's try-with-resources. You can just forget to use using, with, try-with-resources.

There isn't any way to forget to drop a resource if you have RAII.

Comment by throw-the-towel 12 hours ago

Yes, but you also have this problem with `defer`.

Comment by masklinn 9 hours ago

And try/finally.

Comment by pjmlp 8 hours ago

Only when doing stack allocations or reference counting, any other heap management strategy and RAII can't help as well.

For example, combining RAII with lock free data structures, which usually ends up in techniques like hazardous pointers instead.

Comment by okigan 12 hours ago

it's mesmerizing that the fantastic ergonomics of RAII have not propagated to other languages.

It's way easier to use, much clear, less typing, more predictable runtime behavior.

RAII... terrible name, great concept!

Comment by masklinn 9 hours ago

RAII has complications in GC’d, fuzzy ownership langages, it becomes much less ergonomic there because now passing an RAII object as parameter to a function causes that function to clean it up, so you need additional mechanisms to bypass this. Same if you just poke an RAII object inside a collection.

Traditionally langages with simpler runtimes simply used destructors for this, refcounting made it deterministic (modulo the old reference leak), but more advanced garbage collection schemes made that stop working.

Comment by pjmlp 8 hours ago

It only works for stack allocations or when using reference counting, which isn't the ultimate performance of GC algorithms.

Comment by Rohansi 11 hours ago

I mean, sure, but RAII (in C++, at least) is implemented the same way as this article: with a try...finally block!

RAII doesn't really fit into every language because they don't all have deterministic destructors/finalizers and objects with scoped lifetimes. Sometimes you only have one but not the other and you definitely need both.

Comment by okigan 6 hours ago

that's a very broken metaphor - it might be "similar" to try-finally block if you only have one variable/object following RAII. But try-finally block(s) become complete mess when multiple variables are involved, and again requires way more typing/duplication than RIAA. And "similar" in terms of results, NOT similar in terms of performance as try {} block requires additional instructions to set it up, whereas RIAA is runtime free.

And regarding sibling post about finalizers -- another broken concept as you cant use finalizers with limited resources (ex. graphic contexts, db connections, handles, locks, etc) and then these languages that use finalizers become complete mess if you need to manage such resources.

Comment by wannabe44 9 hours ago

A (lexically-scoped) defer is a more general than a finally block. You can express finally-block semantics using defer.

It's also more exception-safe when you have more than one throwing call in the try block.

Comment by healeycodes 8 hours ago

Author here. I find try/finally verbose and vulnerable to mistakes. For me, it adds "function noise".

Comment by gtowey 11 hours ago

Finally isn't bullet proof. If you execute a promise in a try block and it happens to end without resolving, the finally block will be never be executed. Fun stuff.

Comment by anttiharju 11 hours ago

defer isn't bullet proof either. I think there's a linter about it and os.Exit.

One can just wrap os.Exit in a helper to get the expected behaviour.

Comment by Joker_vD 11 hours ago

Also, if you pull the power plug, neither finally blocks nor defers would run. I personally consider it a clear and obvious deficiency in the semantics (and the implementations) of those programming languages but everybody refuses to listen to me.

Comment by cute_boi 11 hours ago

I think this is correct behavior.

``` async function run() { try { await new Promise(() => { // The executor function finishes, // but it never calls resolve() or reject(). }); } finally { console.log("cleanup"); } } ```

I never expect cleanup to be logged.

Comment by codedokode 8 hours ago

Is "defer" a good pattern? Aren't constructors/destructor pairs, like in C++/Rust, or "with" blocks in Python, better, because you cannot forget to call the destructor. I the first code example in the article, it is easy to omit "defer" and the compiler won't notice.

Comment by shortercode 6 hours ago

Defer is easier to fit into TS/JS as GC isn’t observable ( it kinda is with FinalisationRegistry but with a huge list of gotchas ).

Defer only cares if the variable leaves scope. At the end they mention the new “using” syntax which only requires you to mark the declaration, not requiring an additional statement for the cleanup. It’s kinda nice but the resource needs to implement a cleanup method making it less flexible than defer.

Comment by 7 hours ago

Comment by geostyx 12 hours ago

I built a library to do something similar: https://www.npmjs.com/package/yaplib

defer is the thing I miss the most coming from Go

Comment by healeycodes 8 hours ago

Cool! I kinda feel like these should be built-in. But this looks useful for closing the DX gap here.

Comment by fg137 13 hours ago

Would call stack/source map reasonably work with this?

Comment by healeycodes 8 hours ago

Reasonably? Yes. Could probably be made to work well.

  $ bat 2.ts
  ───────────────
     1 │ function one() {
     2 │   let x = 1;
     3 │   defer (() => { throw new Error("thrown from one!"); })()
     4 │   x = 2;
     5 │ }
     6 │ one();
  ─────┴─────────

  $ node 2.js
  ~/healeycodes-typescript-go/2.js:29
            throw _errors_1[0];
            ^

  Error: thrown from one!
      at _callee_1 (/Users/andrew/Documents/GitHub/healeycodes-typescript-go/2.js:9:46)
      at /Users/andrew/Documents/GitHub/healeycodes-typescript-go/2.js:10:33
      at one (/Users/andrew/Documents/GitHub/healeycodes-typescript-go/2.js:22:31)
      at Object.<anonymous> (/Users/andrew/Documents/GitHub/healeycodes-typescript-go/2.js:34:1)
      at Module._compile (node:internal/modules/cjs/loader:1830:14)
      at Object..js (node:internal/modules/cjs/loader:1961:10)
      at Module.load (node:internal/modules/cjs/loader:1553:32)
      at Module._load (node:internal/modules/cjs/loader:1355:12)
      at wrapModuleLoad (node:internal/modules/cjs/loader:255:19)
      at Module.executeUserEntryPoint [as runMain (node:internal/modules/run_main:154:5)

  Node.js v24.15.0