Я пытался получить тот же результат, используя обратные вызовы, промисы, подходы async/await.

Подход обратных вызовов. Это обычный «ад обратных вызовов»

function chain1() {
    console.log('chain 1');
    setTimeout(function () {
        console.log('chain 2');
        setTimeout(function () {
            console.log('chain 3');
        }, 1000);
    }, 1000);
}

chain1();

У нас может быть лучший подход, используя только обратные вызовы и метод связывания.

function chain1(callback) {
    console.log('chain 1', callback);
    if (callback) {
        setTimeout(callback, 1000);
    }
}

function chain2(callback) {
    console.log('chain 2', callback);
    if (callback) {
        setTimeout(callback, 1000);
    }
    return callback;
}

function chain3(callback) {
    console.log('chain 3', callback);
    if (callback) {
        setTimeout(callback, 1000);
    }
}

function chain4(callback) {
    console.log('chain 4', callback);
    if (callback) {
        setTimeout(callback, 1000);
    }
}

chain1(chain2.bind(null, chain3.bind(null, chain4.bind(null, function () {console.log('end', this);}))));

Обещания приближаются

function chain1(time) {
    console.log('chain 1');
    return new Promise(function (resolve, reject) {
        setTimeout(() => {
            resolve()
        }, time)
    });
}

function chain2(time) {
    console.log('chain 2');
    return new Promise(function (resolve, reject) {
        setTimeout(() => {
            resolve()
        }, time)
    });
}

function chain3(time) {
    console.log('chain 3');
    return new Promise(function (resolve, reject) {
        setTimeout(() => {
            resolve()
        }, time)
    });
}

chain1(3000).then(chain2.bind(null, 2000)).then(chain3.bind(null, 1000)).catch(function (r) {
    console.log(r)
});

Асинхронный/ожидающий подход

function chain1() {
    console.log('chain1');
    return new Promise(resolve => setTimeout(resolve, 3000));
}

function chain2() {
    console.log('chain2');
    return new Promise(resolve => setTimeout(resolve, 3000));
}

function chain3() {
    console.log('chain3');
    return new Promise(resolve => setTimeout(resolve, 3000));
}

console.log('start');
(async function () {
    console.log('1');
    await chain1();
    console.log('2');
    await chain2();
    console.log('3');
    await chain3();
})()
console.log('end');

Все эти подходы работают хорошо, но удобнее использовать промисы или async/await.