node.jsasynchronousasynchronous-javascript

can we make synchronous task asynchronous so that we don't invest our whole node process doing the one heavy task and instead continue other tasks?


I have the following code


async function func2() {
    const promise = new Promise((res,rej) => {
        const arr = new Array(10000000).fill('hehehehe');
        const arr2 = JSON.stringify(arr);
        const arr3 = new Array(10000000).fill('hehehehe');
        return res('Completed Execution Of function @2');
    })
    return promise
}

async function func1() {
    const promise = new Promise((res,rej) => {
        const arr = [1,2,3];
        const stringified = JSON.stringify(arr);
        return res('Completed Execution of function @1');
    })
    return promise
}

func2().then(res => console.log(res))
func().then(res => console.log(res))

console.log('I am After Part')

The output of Above script is

the-scripts-output

As per my understanding, both function calls are passed to some services behind the scenes but func1 should get resolved first as there are no heavy calculations, but we see in output func2's response gets printed first and later func1's, What is the reason behind it??

Also, can we purposefully build this behavior in any other way??


Solution

  • There are a few background concepts that need to be covered to understand the behaviour you are seeing.

    1. Tasks and the event loop:

    Javascript has a task queue for managing callbacks and asynchronous behaviour. Each task is run synchronously to completion before the next task is started (see next section).

    JavaScript is single-threaded so only one task will run at a time.
    (Note: for simplicity we're ignoring Workers; having a good understanding of tasks and how the JavaScript runtime works is necessary before looking at how Workers work within JavaScripts execution model)

    The task queue is processed in order (i.e. the oldest task is processed next) and the task must finish before the next task will start.

    Microtasks are special type of task that will run at the end of the current task before the next task runs (all current microtasks are completed before the next task will start).

    Creating a promise (as shown in your example) queues a new microtask.

    For simplicity I won't make any further distinction between tasks and microtasks and will refer to them both as tasks.

    2. JavaScript's "run-to-completion" semantics:

    Once a task has started, all of the JavaScript code within that task will run synchronously to completion before the next task or microtask is run.

    The code can create new promises or add callbacks, but those callbacks will be invoked after the current task has completed.

    3. Here is nodejs's documentation of the event loop. It can give more insight into the details of how the event loop and tasks work.

    4. MDN article introducing asynchronous JavaScript


    With that background we can look at your question to explain what's happening.

    both function calls are passed to some services behind the scenes

    Both functions are queued as tasks to be executed later.

    They are queued in the order that you create the promises:

    func2().then(res => console.log(res))
    func().then(res => console.log(res))
    

    func2 is queued first and func is queued second.

    but func1 should get resolved first as there are no heavy calculations but we see in output func2's response gets printed first and later func1's, What is the reason behind it??

    JavaScript doesn't have any concept of how "heavy" a function is.

    All it knows is that func2 was queued first and func was queued second, so that is the order in which the tasks will run.

    Once the func2 task is started it must finish before the next func task can be started.

    That explains the output you are seeing.


    The way to change the order of output would be to change how the tasks are queued and run.

    A trivial answer is to change the order in which the tasks are queued:

    func().then(res => console.log(res))
    func2().then(res => console.log(res))
    

    But that's not a very interesting answer.

    Instead, lets use our understanding of tasks to change the result.

    We observe that func2 does a lot of work that must be completed before the next task can start.

    So, what if we break up that work into smaller tasks?

    If you can do part of the work in one task, then do the rest of the work in another task, that gives func a chance to run in between.

    Here is an example that splits up the work and uses setTimeout() to queue a new task that completes the work of func2.

    async function func2() {
        const promise = new Promise((res,rej) => {
            const arr = new Array(10000000).fill('hehehehe');
            const arr2 = JSON.stringify(arr);
            
            // the callback passed to setTimeout will be queued 
            // as a new task and executed later.
            setTimeout(() => {
                const arr3 = new Array(10000000).fill('hehehehe');
                return res('Completed Execution Of function @2');
            }, 0);
        })
        return promise
    }
    

    You could make any sort of an async call to queue a new task (e.g. fetch/https request, reading/writing to the filesystem, or making a database call).