Async/await is a language semantics thing. It's not really relevant whether there's a "real" OS thread under the hood, some language level green thread system, or just the current process blocking on something - the syntax exists because sometimes you don't want to block on things that take a long time semantically - I.e. you want the next line of code to run immediately.
You could absolutely write a language where the blocking on long running tasks was implicit and instead there was a keyword for when you don't want to block, but the programmer doesn't really need to care about the underlying threading system.
That's something like what Go does. Goroutines are "green threads" - they can be preempted. There's a CPU scheduler in user space. Go tries to provide "async" performance, and goroutines have minimal state. This seems to work well for the web server case.
Pure "Async" means your application is now in the CPU dispatching business. This works well only if
your application is totally I/O bound and has no substantial compute sections. Outside of that use case, it may be a huge mismatch to the problem. Worst case tends to be programs where almost all the time, something is fast, but sometimes it takes a lot of compute. Then all those quick async tasks get stuck behind the compute-bound operation. Web browsers struggle with that class of problems.
Async I/O tends to be over-used today because Javascript works that way. Many programmers came up from Javascript land. That's the only way they can conceive concurrency. It's a simple, clean model - no need for locks.
Threading is hard. Especially in languages that don't provide much help with locking. Even then, you have lock order problems, deadlocks, starvation, futex congestion...
Advanced JS is a pain for that reason. Any interaction with a "slow" API, even doing cryptographic operations (super common inside libraries), can introduce points at which literally anything can change and in particular a point where new UI events can be triggered. So it's like concurrency but without any of the tools to manage it: you call a function, and by the time it returns arbitrary unrelated stuff may have executed.
I've encountered quite a few programmers over the years who think the absence of tools like locking or concurrent data structures is a feature, that they don't need them because async is simpler. Wrong. They're not unneeded, they're just missing, like many other basic APIs you'd expect that are missing inside browsers.
In standard GUI programming you're in control of the event loop and can decide whether to block it or not. This is a powerful tool for correctness. If you need to do a slow IO in response to a button being clicked you can just do it. The user may see the button freeze in the depressed state for a moment if their filesystem is being slow, for example, but they won't suffer data corruption or correctness issues, just a freeze. If you don't want the UI to freeze you can kick off a separate thread and then use mutexes or actor messaging to implement coordination, doing the extra work that surfaces the possible interactions and makes you work out what should happen.
In the browser environment there's none of that. You have to find other ways to disable the event loop, like by disabling all the UI the user could interact with once an interation starts, or - more commonly - just ignore race conditions and let the app break if the user does something unexpected. It's partly for this reason that web apps always seem so fragile.
And don't get me started on the situation w.r.t. database concurrency ... how many developers really understand DB locking and tx isolation levels?
foo.a = a;
foo.b = await b(); // while waiting for b's completion, something happens that changes the value of `a`, making `foo` invalid
(or more complex variants of this)
In a multi-threaded model, this would be classified as a race condition on `a`. That's why Rust has RefCell for r/w data sharing in single-threaded code.
I don't remember the exact details, but I was hit by this many times when refactoring e.g. db access or file access in the (JS) code of Firefox. This can happen without async/await, without Promise and even without an event loop, you just need callbacks. But of course, having an event loop, Promise and async/await give you way more opportunities to hit such an issue.
Having race condition in js is more about having questionable programming practice though.
Instead of write result of operations into separate variables and aggregate them later. You write them into the same variable with unspecified order and prey they will work correctly. There won't be memory corruption or something. But the results you got won't be correct either.
This type of problems is probably what rust try to address. (Rust will probably tell you to fxxk off because the write permission shouldn't be grant by two place at same time) But unfortunately there isn't rust for js. So only thing you can do is take care of it yourself.
Async/await and threading+blocking are ultimately duals of each other. You can express the same semantics with one model or the other, regardless of the underlying implementation of either. You could in fact implement multi-threading and blocking on top a task-based API if you wanted to - e.g. you could implement a Java style Threads API in JS if you really wanted to (of course, code would still run single threaded, like in old times with single-CPU systems).
You could absolutely write a language where the blocking on long running tasks was implicit and instead there was a keyword for when you don't want to block, but the programmer doesn't really need to care about the underlying threading system.