JavaScript 101 — All about Async Behavior

How JavaScript handles Asynchronous behavior and why it’s so important to understand it.

Fernando Doglio
Bits and Pieces
Published in
8 min readFeb 17, 2023

--

Asynchronous behavior is what happens when you request something to be done “in parallel” to the rest of your program flow. This task will execute separately from the rest of your code, and once it finishes, it needs a way to return the results back into the main program flow.

I know I oversimplified things with that explanation, but consider the following diagram:

Instruction #3 gets executed once the “long-time running instruction” finishes. In the meantime, the rest of the instructions that are not async will get executed.

We could represent this diagram with the following piece of code:

Or if you want a working example, you can do something like this:

I used the setTimeout function to simulate a “long-running task”.

How does async behavior work?

If you want more details, you should know that the JS runtime runs your code inside a single thread (which is why there is a common understanding that JavaScript is single-threaded, even though multi-threading is already possible).

Your code will run sequentially inside that thread, but once you execute a piece of code that is meant to be async, the internal event loop of the runtime will separate that task into “something else” and execute it. Sometimes that “something else” will be a new thread, and others it will be an internal OS mechanism, it all depends on the type of operation you’re trying to do, so it’s best not to duel in exactly how the behavior is resolved, but rather that it is.

Once the separate task is completed, the result is inserted into the event loop, just with the rest of your synchronous instructions. And then once it’s that result’s turn to be pop’ed out of the event loop, you’ll be able to access it.

You can visualize this behavior pretty well with the setImmediate function from Node. That function sets a callback to be executed “immediately”, but if you replace the setTimeout from my previous example with setImmediate , you’ll get the same result.

Why do you think that is?

Because the runtime has already put all console.log lines into the event loop, the setImmediate callback will be placed at the end of the loop.

Why is important to understand async behavior?

In JavaScript, every I/O operation (that is a request that you send, a file that you read, or even a database query that you send) is considered an async operation.

Through this mechanic you can build very efficient systems that can handle thousands of requests per second and that can perform tons of operations without affecting the main workflow (and normally, the user experience that comes with it).

If you don’t take advantage of async behavior or if you misunderstand how to write proper async-ready code, then you’ll be introducing bugs into your system.

So yes, understanding it is very important for any JavaScript developer.

That said, let’s take a look at the tools that JavaScript provides developers to write async-ready code, namely: callbacks, promises and async/await.

Did you like what you read? Consider subscribing to my FREE newsletter where I share my 2 decades’ worth of wisdom in the IT industry with everyone. Join “The Rambling of an old developer” !

How do callbacks work?

Callbacks are the easiest way to visualize async behavior because they’re very much in your face, you can’t miss them.

A callback is nothing more than a function that you pass as a parameter and that gets executed once an async task is completed.

The previous example with the console.log lines shows how callbacks work. Once the setTimeout function is done, it’ll call the anonymous function I set as callback.

But you’ll want to use these functions to also receive the results of asynchronous operations. For example, if you want to read the content of a file using Node, you’ll use the fs.readFile method, which accepts a filename, and a callback as parameters.

This code reads the content of the file ./test1.js and calls the anonymous function that is passed as the second parameter.

This function receives 2 parameters, an error and the content of the file. If there is any issue reading the actual file, then the err parameter will contain all the details. On the other hand, if everything works as expected, the err variable will be empty, and the content parameter will have what we’re looking for (the content of the file).

This error-first pattern when defining callbacks is not only handy, but a widely accepted way of working with callbacks in JavaScript. Essentially, when you’re defining your own callbacks, you’ll want to make sure that the first parameter contains any errors from the async operation that called it.

Callbacks are great, but if you have multiple async tasks, you might run into the feared “callback hell”. It looks like this:

That is not code you want to have to deal with, so instead, you can go with promises.

How do promises work?

A promise is an object that represents the result of an async operation. It is a way to handle async behavior in a more structured and predictable way. It also “looks” better and reads better when you’re going through code that you didn’t write yourself.

A promise has three possible states: "pending," "fulfilled," and "rejected." When an async operation is initiated, the promise is in the "pending" state.

If the async operation is successful, the promise is "fulfilled" (otherwise known as “resolved”) and the resulting value is stored in the promise. If the async operation fails, the promise is "rejected" and the reason for the failure is stored in the promise.

You can use the then method of a promise to specify what should happen when the promise is fulfilled, and the catch method to specify what should happen when the promise is rejected.

Here is an example of using a promise in JavaScript:

The Promise object receives a callback with 2 parameters, the resolve and the reject functions. When your logic inside the promise succeeds, you’ll call the resolve function, which in turn will execute the callback you set using the then method.

If on the other hand, your logic fails or encounters an error, you’ll call the reject function, which in turn will execute the callback you set using the catch method.

Promises are great, but you can also run into nesting callbacks inside then methods. And in the long run your code might become harder to read.

This is when async/await comes into play.

How does async/await work?

Async/await is a way in which you can write promise-based code in a more synchronous-looking way.

Essentially, with async/await you’ll still be dealing with promises, but at least, you’ll write them in a way that looks synchronous.

The underlying behavior is the same, but the code is much easier to read.

Look at the following code:

Notice how the asyncAction function remains the same, but now instead of having to deal with then and catch methods, I’m using a try...catch syntax that should be more familiar to developers coming from other languages.

Essentially, we’re now writing our code sequentially, as if it all was synchronous, when in fact, whatever you have after the await statement (which is how you can signal that a function is asynchronous) is what would be inside the callback you’d set with the then method. And everything inside the catch block, would be inside the callback set with the catch method.

I definitely prefer to use this syntax whenever possible, because it keeps the code as clean as possible and reading it is very much straightforward.

That said, it’s true that one of the major drawbacks of async/await is that you have to use the await statement inside functions you defined as async (notice the definition of the foo function from before). And so far only Deno allows for “top-level awaits”, which essentially means code like this:

Notice how the code is not inside any function. Node doesn’t support that yet.

That said, the other “minor” detail to remember about async/await is that they still work with promises, so if you’re hoping to properly use this syntax, you better understand how promises work.

When should you use each one?

What’s better? Callbacks? Promises? Async/await?

The truth is that they’re all pretty much the same, they all allow you to handle asynchronous behavior.

My recommendation would be:

  • Use callbacks if your code is simple, and you’re not building anything big like a library or a huge system. Also keep in mind that callbacks are not great if know you’ll have to nest asynchronous calls.
  • If on the other hand, you’re building something big, a library for others to use, or your use case requires you to nest multiple I/O calls, then go with promises and async/await.

You might agree or disagree with those recommendations, but to be honest, unless you’re really worried about optimizing every little detail of your code, you’ll be fine with any method.

To summarize, asynchronous behavior happens when you’re working with I/O in JavaScript, whether it’s a database query, reading a file or sending an HTTP request to an external API, they will always happen in parallel to the rest of your program flow.

JavaScript gives you 2, or rather 3 ways to deal with this behavior, you either go with callbacks, promises, or their cousins, async/await syntax.

If you have any questions about all of this, feel free to leave a comment below and I’ll do my best to answer them!

Build Apps with reusable components, just like Lego

Bit’s open-source tool help 250,000+ devs to build apps with components.

Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.

Learn more

Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:

Micro-Frontends

Design System

Code-Sharing and reuse

Monorepo

--

--

I write about technology, freelancing and more. Check out my FREE newsletter if you’re into Software Development: https://fernandodoglio.substack.com/