How to make code which uses Lightning promises look more synchronous

You have two basic problems. First, you don't need $A.getCallback unless you need to call component.set. At all other points, it's superfluous.

main: function(component, helper) {
    helper.promise1(component, helper).then(
        function(record) {
            return helper.promise2(component, helper, record).then(
                function(){
                    console.log("Success");
                    helper.showToastMessage(component, "Great Success!!!!")
                }, 
                function(errorMessage){
                    console.log("Rejected by second promise2");
                    helper.showToastMessage(component, "Failure " + errorMessage);
                }
            )
        },
        function(errors){
            console.log("Rejected by first promise1");
            let message = errors && Array.isArray(errors) && errors.length > 0 ? errors[0].message :'Unknown error';
            helper.showToastMessage(component, "Failure " + message);
        }
    )
}

Next, you typically chain a then from the previous then:

main: function(component, helper) {
    helper.promise1(component, helper).then(
        function(record) {
            return helper.promise2(component, helper, record);
        },
        function(errors){
            console.log("Rejected by first promise1");
            let message = errors && Array.isArray(errors) && errors.length > 0 ? errors[0].message :'Unknown error';
            helper.showToastMessage(component, "Failure " + message);
        }
    ).then(
        function(){
            console.log("Success");
            helper.showToastMessage(component, "Great Success!!!!")
        }, 
        function(errorMessage){
            console.log("Rejected by second promise2");
            helper.showToastMessage(component, "Failure " + errorMessage);
        }
    )
}

Also, you typically want to use catch instead of the two-parameter then version, which lets you have as single error handler:

main: function(component, helper) {
    helper.promise1(component, helper).then(
        function(record) {
            return helper.promise2(component, helper, record);
        }
    ).then(
        function(){
            console.log("Success");
            helper.showToastMessage(component, "Great Success!!!!")
        }
    ).catch(
        function(errorMessage){
            console.log("Error occuring during chain");
            helper.showToastMessage(component, "Failure " + errorMessage);
        }
    );
}

As a bonus, arrow functions makes things a bit more legible:

main: function(component, helper) {
    helper.promise1(component, helper).then(
        record => helper.promise2(component, helper, record);
    ).then(
        () => {
            console.log("Success");
            helper.showToastMessage(component, "Great Success!!!!")
        }
    ).catch(
        (errorMessage) => {
            console.log("Error occuring during chain");
            helper.showToastMessage(component, "Failure " + errorMessage);
        }
    );
}

After that, you might want to remove the debug logs for production use:

main: function(component, helper) {
    helper.promise1(component, helper).then(
        record => helper.promise2(component, helper, record)
    ).then(
        () => helper.showToastMessage(component, "Great Success!!!!")
    ).catch(
        errorMessage => helper.showToastMessage(component, "Failure " + errorMessage)
    );
}

This is what you'd typically see in production-ready code (plus comments, as necessary).


Looks like I have found some possible solution to beautify this code.

main: function(component, helper) {
    helper.promise1(component, helper).then(
        $A.getCallback(helper.promise2),

        $A.getCallback(function(errors){
            console.log("Rejected by first promise1");
            let message = errors && Array.isArray(errors) && errors.length > 0 ? errors[0].message :'Unknown error';
            helper.showToastMessage(component, "Failure " + message);
        })

    ).then(
                $A.getCallback(function(){
                    console.log("Success");
                    helper.showToastMessage(component, "Great Success!!!!")
                }), 
                $A.getCallback(function(errorMessage){
                    console.log("Rejected by second promise2");
                    helper.showToastMessage(component, "Failure " + errorMessage);
                })

    )
}

However, if we need component and helper variables in the second promise then we have to tweak resolve statement and change it from

resolve(record)

to

resolve({'c':component, 'h':helper, 'r':record})

and to tweak the second promise from

sendMessagePromise : function(component, helper, record) {
    ...

to

sendMessagePromise : function(value) {
    let component = value.c, helper = value.h, record = value.r;

Also using suggestions from sfdcfox I could rewrite this as following:

main: function(component, helper) {
    helper.promise1(component, helper).then(
        promise2
    ).then(
        () => helper.showToastMessage(component, "Great Success!!!!")
    ).catch(
        errorMessage => helper.showToastMessage(component, "Failure " + errorMessage)
    );
}