- Async/await relies on promises to offer more readable asynchronous code, closer to the traditional synchronous style.
- Async functions always return a promise and allow you to use await to pause their execution without blocking the main thread.
- Error handling with try/catch and utilities like Promise.all makes it easier to control failures and combine several operations in parallel.
- The combined use of promises, async/await, and APIs like fetch is the foundation of modern network-oriented JavaScript development.
La programming asynchronous in JavaScript has become indispensable in modern web development: API requests, access to databasesFile reading or any operation that takes more than a moment depends on it to keep the interface running smoothly. Thanks to the promises, async/await and the APIs We have a very powerful set of tools to control that flow without blocking the application.
For years the pattern was mainly used callbacks and chained promiseswhich, although they work very well, can end in the infamous "callback hell" or in endless chains of .then() y .catch() difficult to follow. With the arrival of async/await in ECMAScript 2017JavaScript took a huge leap in readability, allowing asynchronous code to be written in a style almost identical to traditional synchronous code.
What is asynchronous programming in JavaScript and why is it so important?
La asynchronous programming It's a way of organizing code so that a task can begin and the program can continue executing other things without getting "frozen" waiting for a response. In JavaScript, this is vital because the language runs, in the browser and in Node.js, on a single main thread which addresses both logic and interface.
When you throw a HTTP request to an APIWhen you read a file or query a database, these operations can take anywhere from milliseconds to several seconds. If they were synchronous, the user would see the page locked and unable to interact. With the asynchronous model, the JavaScript engine delegates these tasks and can continue working in the meantime. painting the UI, handling events, and running other scripts.
Originally, the standard mechanism for reacting when an operation ended was to pass a callback function as an argument. That function was executed later with the result or the error. The problem is that, by chaining callbacks within other callbacks, the dreaded callback hell: code with too much indentation, difficult to read, debug and maintain.
To solve some of that chaos, the following appeared: promiseswhich encapsulate the result of an asynchronous operation and allow actions to be chained together with .then() and manage failures with .catch()This was already a great improvement, but it still left the code full of callbacks "disguised" behind those methods.
Promises: the foundation of async/await
Before understanding async/await, it is essential to be very clear about what a promise in JavaScriptA promise is an object that represents the future result of an asynchronous operation: something that has not yet happened but will happen later, either successfully or unsuccessfully.
An standard promise can be found in one of three well-defined states: pending (pending) while the operation is in progress, completed (Fulfilled) when it ends well and produces a value, or is rejected (rejected) when it fails and generates an error. That state of the promise determines which callbacks will be executed next.
When you create a promise using the constructor PromiseYou pass it an executable function that receives two arguments: resolve y rejectIf the operation goes well, you call resolve(valor)If a problem occurs, you invoke reject(error)Then, from the outside, you can use miPromesa.then() to process the result and .catch() to manage errors.
In addition to hand-crafted promises, many modern language APIs already They fulfill a series promise.. The function fetch()For example, it initiates an HTTP request and immediately returns a promise that will eventually resolve with an object. Response or rejected if there is a network failure.
To work with multiple tasks in parallel, JavaScript offers utilities such as Promise.all()This method takes an array of promises and returns a new promise that resolves only when all promises have successfully completed, or is rejected if any of them fail. This is especially useful when you want combine data from different sources before further.
Key concepts: async, await, and the error model
The syntax of async/await is built on promisesIt doesn't replace them. What it does is provide a more convenient, clearer way, closer to synchronous programming, to work with them, reducing the visual noise of .then() chained.
The key word async It is placed before a function to tell the JavaScript engine that the function will work asynchronously and that, whatever happens inside, will always keep a promiseEven if you return a normal value, the engine will automatically wrap it in Promise.resolve(valor).
For its part, the reserved word await can only be used within a function declared as asyncIts function is to pause execution within that function until the promise passed to it is fulfilled. be resolved or rejected, directly returning the resolved value or throwing an exception if the promise ends with an error.
when you use await promesa and the promise resolves, you get the result as if it were a synchronous call. If instead the promise is rejected, An exception is generated at that point., equivalent to doing a throw errorThis allows the reuse of the same error handling structures as in synchronous code, such as try..catch.
By wrapping our asynchronous calls in a block try { ... } catch (error) { ... }We can capture cases where any of the promises fail, display appropriate messages to the user, or make decisions such as retrying, redirecting, or logging the failure to an external system.
The keyword async: functions that always return promises
Place async in front of a function This has two very important consequences: on the one hand, it forces the return value to always be a promise; on the other, it enables the use of await within the body of that function, which completely changes how the asynchronous flow is structured.
If you declare something like async function sumar() { return 1; }Although it may seem like a normal function, what you actually get is a function that returns a previously resolved promise with the value 1Outside of your system, you will need to access that result using await sumar() or with sumar().then() if you prefer the chained style.
You can also explicitly return a promise from a function. async, for example return Promise.resolve(1);and the behavior will be the same. In both cases, the function is aligned with the promise model, which facilitates its combination with other asynchronous functions and utilities such as Promise.all().
It's very common that, when we're starting out, we forget to mark a function as such async and yet we use await inIn that situation, JavaScript will throw a syntax error indicating that await This is only valid in asynchronous functions (or in modules that support it at a higher level). This message is a reminder that async y await they always go hand in hand.
In environments where we don't use modules or have to support older browsers, there is a widespread pattern that consists of wrap the code inside a self-executing async function, something like (async () => { ... })();In this way, the entire block remains within an asynchronous context without "contaminating" the global space.
How await works and what it actually does with promises
The real change of mindset comes with awaitWhen the JavaScript engine encounters const resultado = await algunaPromesa; within a function async, pause the execution of that function at that point and returns to the event loop to continue attending to other tasks.
Meanwhile, the promise continues its course independently. When it finally resolves, the engine resumes execution of the function right after the line with await, assigning to the variable the value returned by the promiseFrom the perspective of someone reading the code, it looks like a synchronous call that just takes a little longer.
What's interesting is that this pause It does not block the CPUThe main JavaScript thread can process user events, animations, other asynchronous code, etc. That's why we say that await is more a elegant syntactic abstraction about the promises of a new way of running tasks in the background.
If you prefer, you can imagine that the combination const resultado = await promesa; es promesa.then(valor => { resultado = valor; ... }) of something equivalent, but distributed in a linear way that avoids nested callbacks and improves flow clarity.
One curious detail is that, if you move to await an object that is not a promise but has a method .then()The engine will try to treat it as if it were. It will invoke that .then() with two internal functions of resolution and rejection very similar to those of the builder of Promise. This way, await It is compatible with implementations of "thenable" type promises.
Error handling with async/await: try/catch and rejected promises
When a promise is reject while you are using awaitThe direct effect is that an exception is thrown on that line. From your code's perspective, it's as if you had written throw error; right where the await, which causes that execution to jump to the block catch closest.
For example, if you use const datos = await obtenerDatos(); and the function obtenerDatos It returns a promise that ends in an error; the flow will go directly to the block catch (error) your try..catchIf you don't have a capture block around it, the implicit promise that the function returns async It will be rejected.
This means that, although there may still be promises inside, when you work with async/await the natural way to handle errors it's with try..catchas in any other JavaScript code that throws exceptions. This unification of styles greatly simplifies the logic and reduces the need to remember different patterns depending on whether the code is synchronous or asynchronous.
If, by mistake, you forget to add a .catch() to the promise returned by a function async If it fails, the environment will display a warning. “unmanaged promise” (unhandled promise rejectionIn addition, you can use the global event in the browser. unhandledrejection to register a handler that captures all those errors and prevents them from going unnoticed.
When you combine await with utilities such as Promise.all, a single mistake in one of the promises will propagates the joint promise and finally results in an exception that you can catch with try..catchIn this way, managing errors in complex workflows remains consistent and centralized.
Async/await with fetch: step-by-step API calls
One of the most common uses of async/await is accessing HTTP APIs using the function fetch()This API returns a promise that resolves to an object Response, on which you can then invoke methods such as .json() o .text()which in turn bring new promises.
Imagine you want to encapsulate the logic of requesting data from an endpoint in a function. If you declare async function obtenerDatos(), within it you can wait for the network response with await fetch(...) and then transform the body into JSON with const datos = await respuesta.json(); without needing to chain several .then().
To ensure that the server has responded correctly, it is common to check the property respuesta.okwhich indicates whether the HTTP status code is in the range 200-299. If not, you can manually throw an error with throw new Error('Error en la red');, which will cause the catch capture the situation.
Organize the entire flow within a try { ... } catch (error) { ... } This makes the asynchronous function much more resilient: any problem, whether network, JSON parsing, or logical, will trigger the capture block where you can log the error or display a more user-friendly message.
Ultimately, to use the function, simply call it like any other: obtenerDatos();Remember that, being asynchronous, it returns a promise, so from the outside you'll have to choose whether to use it. await obtenerDatos() within another function async or handle it with .then().catch() if you are in a non-asynchronous context.
Execute multiple operations in parallel with Promise.all and async/await
Often you don't want to wait for one request to finish before starting the next; instead, you're interested in... launch multiple asynchronous operations at once and continue when they have all finished. For that, the method Promise.all() fits perfectly.
Suppose you need to consume two different APIs, for example /datos1 y /datos2, and later merge the information into a single objectInstead of waiting for the first one to complete before starting the second, you can write something like const [r1, r2] = await Promise.all([fetch(url1), fetch(url2)]); and both applications will be made in parallel.
Once you have both answers, you can apply await about each one to transform them into JSON: const d1 = await r1.json(); const d2 = await r2.json();Finally, it is very common combine the data with the propagation operator: const datosCombinados = { ...d1, ...d2 };, which creates a unique structure that blends the properties of both.
This entire block should be wrapped in a try..catchbecause any failure in one of the requests will cause Promise.all() is rejected. Thanks to async/await, handling these types of scenarios with concurrent operations It is visually simple but very powerful in terms of performance.
This pattern is especially useful when your web application requires several independent resources before a view can be rendered: for example, obtaining configuration, user data and usage statistics simultaneously, reducing the overall wait time.
Real advantages of async/await versus “pure” promises and callbacks
The use of async/await offers a number of practical benefits These benefits are noticeable from the earliest examples to large-scale applications. The most obvious is readability: the flow is written from top to bottom without the need for nested or chained functions. .then() one after the other.
Another strong point is the code maintainabilityWith more linear blocks, it's easier to introduce changes, extract parts into auxiliary functions, or reuse logic. This directly results in fewer errors and a smoother learning curve for newcomers to the project.
Error handling with try..catch instead of multiple .catch() scattered It helps centralize fault logic. You can surround several await with a single block and implement a consistent error management policy, without losing the detail of which line the problem occurred on.
From a performance standpoint, async/await doesn't make code "faster" on its own, but it can encourage you to use patterns like Promise.all() to execute tasks in parallel where you might have previously run them serially. The result is less total waiting time for the end user.
Furthermore, async/await is compatible with traditional promisesYou don't have to rewrite all your code to adopt it. You can combine functions that use it. .then() with others that use await No problem, which makes it easier to migrate projects gradually.
In the modern ecosystem, both in browsers and in Node.js, most tools and libraries already rely on promises and async/await. This makes learning these concepts well not optional, but essential. a requirement for working with modern JavaScript in professional projects.
This whole set of ideas—promises, async/await, fetch, Promise.allThe try/catch and glossary of key terms form a very solid framework for writing clear, readable, and robust asynchronous codeMastering them allows you to build modern web applications that respond quickly, take good advantage of the network, and are much easier to maintain over time.
Passionate writer about the world of bytes and technology in general. I love sharing my knowledge through writing, and that's what I'll do on this blog, show you all the most interesting things about gadgets, software, hardware, tech trends, and more. My goal is to help you navigate the digital world in a simple and entertaining way.