This needs to be emphasized further. Erlang is a platform for building distributed systems. In a distributed system, you don't have your state management and your heavy IO/computation occurring in the same process. They aren't usually even co-located on the same machine, where there would be any chance of them competing for resources.
Instead, in a well-engineered distributed system, you have a control plane which makes lots of branchy decisions based on command/query-like input, and a data plane that sits working on long tasks (e.g. computing results, streaming IO) until the control plane tells it to stop.
Erlang is a platform for writing control-plane software. It gives you plenty of tools (NIFs, ports, C-nodes, simple binary protocol parsing) to write data-plane software to integrate with it, but it always implicitly expects the control-plane/data-plane separation.
What does this mean for using Erlang to power, say, a GUI client application, or a 3D game? It means that you don't try to cram your fancy GUI rendering into the Erlang emulator's process-space, or vice-versa. Rather, the GUI acts as a client, Erlang acts as a (local) server, they communicate over a socket, and each process gets to keep its event loop in a sane state.
My "secret weapon" in rapid native-client application prototyping right now is an atom-shell client that ships with an embedded Elixir node. Node.js starts up, spawns the Erlang VM, connects to it with plain TCP, and then opens a webview which connects to it again over HTTP. The web-view becomes basically an extremely fancy, scriptable TTY for the Erlang VM to read from and write to, while Node.js is doing more high-level non-sandboxed interactions over the regular TCP socket.