It can be. Used judiciously, they can reduce boilerplate and help highlight the important parts of the codebase.
The problem is that they're catnip to a certain type of developer. If they're allowed to proliferate in an uncontrolled way, you can easily end up with unmaintainable code.
Worst case, you'll end up with someone implementing a half-arsed type system at which point you may as well delete the repository (ok, slight exaggeration).
I can't really comment on whether or not JS would be a better language with a good macro system. My guess is probably not but I have no evidence for that.
For example, macros and functions are nearly the same thing: one runs at compile time, the other at runtime. But they both have documentation, and you can jump to a macro's definition the same way you would for a function. You can also compose them together and generate new ones on the fly, etc.
Basically, a lot of the problems with macros in other languages is that macros are considered this special type of not-really-a-function. It's either templating sugarcoat, or restricted turing-noncomplete layer. But if you treat macros as literally functions that just happen to run at compile time, the problems are reduced. And if you treat them with discipline and document what they do, and show examples, and add test cases, then you never have to worry -- just like normal software.
One helpful rule of thumb: If you copy some functionality more than twice, it's probably time to make it a function. If you define a set of functions repeatedly -- like test cases -- or find yourself writing the same pattern over and over, or if you can generate most of your structure automatically based on e.g. "I know this is a React component", it might be time to write a macro.
If this concept sounds intriguing to anyone, I highly encourage you to check out Lumen: https://github.com/sctb/lumen
It's a Lisp that's literally javascript:
Compiled code: https://github.com/sctb/lumen/blob/master/bin/reader.js
Original code: https://github.com/sctb/lumen/blob/master/reader.l
So if you understand Javascript, then there's not much to learn. And you can use it in your own projects right away.
Most of Lumen's brevity is thanks to macros.
(One hack for quickly understanding Lumen: read the tests. Every language feature is meticulously demo'd: https://github.com/sctb/lumen/blob/master/test.l)
Simula-style languages like C++ and Java don't offer the same sorts of dynamic features.
> Whereas (I feel that) macros are undebuggable by design.
I'd be interested to know what you mean by this. I've written macros in Emacs Lisp and Racket, and both were pretty easy to debug. In particular the tooling for each (Emacs and DrRacket) can "expand macros" to show what code a particular macro usage will turn into (at which point, we're back to debugging non-macro code; although potentially messier than we'd right by hand). In that sense they're more debuggable than functions, since we can't calculate the result of a function until run time (which is why functions need test suites, to provide a run time that's not the production application).
Essentially it says that we understand the things we're used to using, but not those we're not. Hence we can appreciate, instinctively, the problems caused by languages which lack a thing we're used to (for example, the standard "Go needs generics" arguments). Yet it's harder for us to appreciate those same problems if we're not experienced with that feature (e.g. someone who's only ever programmed in Go).
Closures were another step up the ladder of abstraction, and now it's hard to imagine not being able to use them.
Macros are no different. It seems worth being skeptical of the idea that it's better for a programmer to have less power. And Emacs Lisp shows macros can work at scale without causing friction.
To your point on debugging, it all depends on the language. Elisp has an excellent debugging facility: https://www.gnu.org/software/emacs/manual/html_node/elisp/Ed...
The key is to internalize that macros are no different from functions. If you know how to call a function, you know how to use macros. The only difference is that they return code rather than values.
But that's often not the case. In fact, OO is a nice example. It's probably less true now that OO seems to be a bit out of fashion, but the number of crappy, poorly documented OO systems I've seen in Lisps over the years is staggering.
It's not really a criticism of macros so much as an observation. It's also possible that the type of developer who is attracted to adding a bijou OO system into their application is the type of developer that is attracted to Lisps. It may well be the case that adding a macro system to JS wouldn't have the same attraction so wouldn't have the same downside.
Wrong, there are many macros that are impossible to grok without reading the docs carefully, there are also scoping rules. They are nowhere near at the usability of functions, macros should be written judiciously. Most uses of macros can be eschewed by higher-order functions, unless you are making a language.
Proper macros (Lisp, Scheme, Julia, etc.) are just as readable as functions, since they basically are functions that operate on ASTs.
> Versus proper compiler features (async, =>, type system) + a proper code design.
Macros are a proper compiler/interpreter feature, and enable proper code design.
Relying on the language in place of the facility to write reusable macros is like relying on stdlib in place of the facility to write reusable functions. Sure, ideally you want the common needs to be covered out of the box, but not everything is common enough needs where the solution space is well-enough explored for a standard solution to be the right choice even as a default.