JavaScript intermediate: async, fetch, modules, and real-world patterns

JavaScript · July 25, 2026
JavaScript intermediate: async, fetch, modules, and real-world patterns

In the beginner lesson you learned the fundamentals: variables, functions, arrays, objects, the DOM, and event listeners. Now we are going to talk about the parts of JavaScript that show up in every real application: handling asynchronous operations like network requests, splitting your code into modules, and the patterns that make code maintainable as it grows.



Why asynchronous matters


JavaScript runs on a single thread. That means long-running operations like network requests, file reads, or timers cannot block the whole program. When you fetch a JSON document from a server, the browser does the network work in the background, and your code gets notified when the response is ready. Until then, the rest of the page keeps working. This is the asynchronous model.



The old way to handle this was callbacks. A function would do its work, then call another function when done. Callbacks work for one level of nesting, but as soon as you need to chain several operations together you get a wall of nested function calls that nobody can read. It is affectionately called "callback hell".



<code>// The old way: nested callbacks
getUser(userId, function (user) {
getPosts(user.id, function (posts) {
getComments(posts[0].id, function (comments) {
// finally do something with the data
});
});
});</code>


Do not write code like that. Modern JavaScript has a much better way.



Promises: a cleaner model for async work


A promise is an object that represents a value that will exist in the future. It can be in one of three states: pending, fulfilled (success), or rejected (error). You attach handlers to each outcome.



<code>fetch(&quot;https://api.example.com/data&quot;)
.then(function (response) {
return response.json(); // returns a new promise
})
.then(function (data) {
console.log(data);
})
.catch(function (error) {
console.error(&quot;Failed:&quot;, error);
});</code>


The fetch function returns a promise. The first .then runs when the response headers arrive, and it returns another promise (from response.json()). The second .then runs when the JSON is fully parsed. The .catch runs if anything in the chain throws.



This is much better than nested callbacks, but we can do even better.



async / await: the modern way


The async and await keywords let you write asynchronous code that looks like synchronous code. Behind the scenes it is still promises, but the syntax is flat and readable.



<code>async function loadDashboard() {
try {
const user = await fetch(&quot;/api/me&quot;).then(r =&amp;gt; r.json());
const posts = await fetch(&quot;/api/posts&quot;).then(r =&amp;gt; r.json());
const weather = await fetch(&quot;/api/weather&quot;).then(r =&amp;gt; r.json());

renderUser(user);
renderPosts(posts);
renderWeather(weather);
} catch (err) {
showError(err);
}
}</code>


An async function always returns a promise. The await keyword pauses the function until the awaited promise settles. If the promise rejects, the error becomes a normal throw that you can catch with a try/catch block.



Use this pattern for any sequence of network calls. The readability win is enormous.



fetch: the modern way to make network requests


The fetch API is built into every browser. It returns a promise. It supports GET, POST, PUT, DELETE, and every other HTTP method. It handles JSON, form data, file uploads, and streaming responses.



<code>// GET
const res = await fetch(&quot;/api/articles&quot;);
const articles = await res.json();

// POST
const res = await fetch(&quot;/api/articles&quot;, {
method: &quot;POST&quot;,
headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
body: JSON.stringify({ title: &quot;New&quot;, content: &quot;...&quot; }),
});
if (!res.ok) throw new Error(&quot;Save failed&quot;);
const saved = await res.json();</code>


Note the res.ok check. fetch only rejects on network errors, not on HTTP 4xx or 5xx. You must check the status yourself, otherwise a 404 silently becomes an empty {} and you spend an afternoon wondering why your code is broken.



Splitting your code into ES modules


Once your script gets past a few hundred lines, you want to split it into multiple files. Modern JavaScript has ES modules, which let you do that with explicit imports and exports.



In your HTML, declare the script as a module:



<code>&amp;lt;script type=&quot;module&quot; src=&quot;app.js&quot;&amp;gt;&amp;lt;/script&amp;gt;</code>


In app.js, import the things you need:



<code>import { fetchJSON } from &quot;./api.js&quot;;
import { renderArticle } from &quot;./render.js&quot;;
import { setupNav } from &quot;./nav.js&quot;;

setupNav();

const articles = await fetchJSON(&quot;/api/articles&quot;);
renderArticle(articles[0]);
</code>


In api.js, export the function:



<code>export async function fetchJSON(url) {
const res = await fetch(url);
if (!res.ok) throw new Error(&quot;HTTP &quot; + res.status);
return res.json();
}
</code>


Modules are evaluated once, cached, and the imports are statically analyzable. Tree-shaking tools can drop unused code. Every modern bundler expects modules. There is no reason to use the old script-tag-everything approach for new code.



Common intermediate mistakes


Forgetting to await. A function that returns a promise is not the same as the value the promise resolves to. If you forget await, you get the promise object, not the data, and your code breaks in confusing ways.



<code>// wrong
const data = fetchJSON(&quot;/api/articles&quot;);
data.forEach(render); // TypeError: data is a Promise, not an array

// right
const data = await fetchJSON(&quot;/api/articles&quot;);
data.forEach(render);</code>


Using var. The old var keyword has weird scoping rules. Always use const for things that do not get reassigned and let for things that do. There is no reason to use var in modern code.



Mutating data you do not own. When you receive a list of articles from the server, treat it as read-only. If you need a different version of it, make a copy first: const myArticles = [...articles]. Mutating inputs makes the rest of the program impossible to reason about.



Building UI by string concatenation. It is tempting to do el.innerHTML = "&lt;li&gt;" + name + "&lt;/li&gt;". It works, but it is also a security hole: if name contains a &lt;script&gt; tag, you have just executed arbitrary code. Use textContent or the safer document.createElement / appendChild pattern.



The shape of a real feature


Putting it all together, a typical interactive feature has this shape:



<code>// 1. Find the relevant DOM elements
const form = document.querySelector(&quot;#contact-form&quot;);
const status = document.querySelector(&quot;#status&quot;);

// 2. Wire up an event listener
form.addEventListener(&quot;submit&quot;, async (event) =&amp;gt; {
event.preventDefault(); // stop the page reload
status.textContent = &quot;Sending...&quot;;

// 3. Gather the data
const data = Object.fromEntries(new FormData(form));

// 4. Send it to the server
try {
const res = await fetch(&quot;/contact&quot;, {
method: &quot;POST&quot;,
headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error(&quot;Server said no&quot;);

// 5. Update the UI to reflect success
status.textContent = &quot;Message sent!&quot;;
form.reset();
} catch (err) {
// 6. Update the UI to reflect the error
status.textContent = &quot;Could not send: &quot; + err.message;
}
});</code>


Every interactive feature in a real application looks roughly like this. Find elements, listen for events, gather data, send it, update the UI, handle errors. Once you have the pattern, you can build anything.



What is next


You can now write modern JavaScript: async functions, fetch, modules, and the pattern of an interactive feature. The remaining skills are mostly about taste: how to structure a larger application, how to test your code, how to debug when something is not working, and how to keep the bundle size small as you grow. Those are not language features, they are engineering practices, and you pick them up by building real things.


← More in JavaScript