最後に、同期の話と非同期の話の境目を 1 つだけ確かめます。setTimeout や Promise.then に渡した関数は、それを登録した関数(a や b)の上では実行されません。

同期のスタックが空になってから、Promise と setTimeout のコールバックが順に実行される

// async.js
function names() {
  return new Error().stack.split('\n').slice(2)
    .map((line) => line.trim().split(' ')[1]).join(' <- ');
}

function b() { console.log('同期の b の中:', names()); }
function onTimeout() { console.log('setTimeout:', names()); }
function onFulfilled() { console.log('Promise.then:', names()); }

function a() {
  b();
  setTimeout(onTimeout, 0);
  Promise.resolve().then(onFulfilled);
}
a();

同期の b の下には a とスクリプト本体が積まれていますが、onFulfilled と onTimeout の下に a はいません。仕様も、こうした後回しの処理(ジョブ)は「実行コンテキストスタックが空になったとき」に動くと定めています。一番下に見える processTicksAndRejections や processTimers は Node 内部の関数です。仕様の言う「空」と Node 内部の関数の関係は、この記事では調べていません。