Your comment is phrased as a rebuttal, but I actually agree with it wholeheartedly, with one minor quibble.
> Blocking threads are a much more natural way of thinking about concurrency.
The (ideal) programming model for writing a thread vs an async task is identical, and indeed the most natural way of writing sequential flows that may incidentally be temporally separated. A blocking operation and an await are equivalent (in the absence of shared memory; in its presence they differ in that an async task on a single thread gets to assume atomicity of the code between awaits, but that's not too relevant here). I think what Joe is arguing against here is exactly splitting up your business flows into a bunch of explicit state machine transitions.
The difference is only in the caller. Callers of async tasks (à la Rust) have the right and responsibility of controlling how (and whether) the task they create resumes, while threads/BEAM processes (and ‘stackful’ futures à la Go, JavaScript, etc.) are answerable only to the great global event loop in the sky (sometimes known as the OS scheduler or the BEAM multiplexer, etc.). This makes it easier to avoid bugs that are about forgetting to drive a task, but harder to avoid bugs that are about forgetting to stop a task, or stopping a task wrongly; see structured concurrency. It also hurts composability a bit, because by construction there can be only one event loop and there's no way to decompose it.
You can see Erlang (or I suppose we should talk about BEAM more generally now people like Elixir) in terms of OS processes as an analogy, but it's just as valid and perhaps a little more accurate to think about it as a pool of async tasks with a global event loop, à la Go, where all blocking-looking code is actually async. It shares a common problem of such languages that because it can't interrupt real blocking code, e.g. FFI, you have to be very careful not to accidentally starve your other processes with such a call. This is made more obvious (but not really fixed) by languages that colour async functions differently, because the async colour acts as a promise (sadly not enforced by the language) that the function will be a good neighbour and not block for too long (whatever that means).