◆ 内部 (libuv) はマルチスレッド (デフォルト 4)
◆ IO 処理は並列処理されるので await とかしないと書き込み順は保証されない
◆ 環境変数でスレッド数を 1 にしたら JavaScript で書いたとおりの順番

前にここでも書きましたが fs.appendFile を待機せずに実行すると書き込み順がバラバラになります

const fs = require("fs")

const w = text => fs.appendFile("./log.txt", text, () =>{})

for (const i of Array(30).keys()) {
w(i + "\n")
}
0
1
3
4
5
7
8
6
2
10
9
11
14
15
16
17
12
19
13
20
22
23
24
18
25
27
28
29
21
26

Node.js がシングルスレッドと言ってもそれは JavaScript レイヤーの話です
内部の libuv ではマルチスレッドで実行されています
デフォルトは 4 スレッドで処理されます (最大で 1024 まで増やせます)
それを超えるタスクが来るとキューに入りますが 4 つまでは並列で処理されるので書き込み順が JavaScript での実行順にならないわけです

このスレッド数は環境変数 UV_THREADPOOL_SIZE で変更できるので これを 1 にしてみます
コマンドは Windows のコマンドプロンプトで write.js の中身は上の JavaScript のコードです

set UV_THREADPOOL_SIZE=1 & node write.js
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

キューを使って 1 つずつ IO 処理が行われるのでちゃんと連番になりました

とは言えスレッド数を減らすのはパフォーマンスに影響しますし このために減らすのは良い方法ではありません
前の記事で書いたような JavaScript 側でキューを作ることもできますが 基本はエラーハンドルすべきなので非同期処理を待ってから次の処理に行くのが良いと思います