In my opinion you must have function coloring, it's impossible to do async (in the common sense) without it. If you break it down one function has a dependency on the async execution engine, the other one doesn't, and that alone colors them. Most languages just change the way that dependency is expressed and that can have impacts on the ergonomics.
Not necessarily! If you have a language with stackful coroutines and some scheduler, you can await promises anywhere in the call stack, as long as the top level function is executed as a coroutine.
Take this hypothetical example in Lua:
function getData()
-- downloadFileAsync() yields back to the scheduler. When its work
-- has finished, the calling function is resumed.
local file = downloadFileAsync("http://foo.com/data.json"):await()
local data = parseFile(file)
return data
end
-- main function
function main()
-- main is suspended until getData() returns
local data = getData()
-- do something with it
end
-- run takes a function and runs it as a coroutine
run(main)
Note how none of the functions are colored in any way!
For whatever reason, most modern languages decided to do async/await with stackless coroutines. I totally understand the reasoning for "system languages" like C++ (stackless coroutines are more efficient and can be optimized by the compiler), but why C#, Python and JS?
Look at Go or Java virtual threads. Async I/O doesn't need function coloring.
Here is an example Zig code:
defer stream.close(io);
var read_buffer: [1024]u8 = undefined;
var reader = stream.reader(io, &read_buffer);
var write_buffer: [1024]u8 = undefined;
var writer = stream.writer(io, &write_buffer);
while (true) {
const line = reader.interface.takeDelimiterInclusive('\n') catch |err| switch (err) {
error.EndOfStream => break,
else => return err,
};
try writer.interface.writeAll(line);
try writer.interface.flush();
}
The actual loop using reader/writer isn't aware of being used in async context at all. It can even live in a different library and it will work just fine.
Uncoloured async is possible, but it involves making everything async. Crossing the sync/async boundary is never trivial, so languages like go just never cross it. Everything is coroutines.