Modern JavaScript provides powerful features to build well-structured and responsive web applications.
Modules allow developers to organize code into reusable files using import and export, improving readability and maintainability.
Promises and async–await simplify handling asynchronous operations, making code easier to understand and manage without deeply nested callbacks.
The Fetch API enables asynchronous communication with servers, allowing applications to send and receive data dynamically without reloading the page.
ES6 Modules: import/export
Modules let you split code into reusable files, eliminating global scope pollution and enabling tree-shaking for smaller bundles.
Introduced in ES6 and now supported by all browsers, they follow the same import/export syntax across frontend and backend.
Declaring and Exporting Modules
Exports make functions, classes, or variables available to other files. Use named exports for multiple items or default exports for single primary exports.
Export Patterns:

Complete example (math.js)
// Named exports
export const PI = 3.14159;
export function circleArea(radius) {
return PI * radius * radius;
}
// Default export
export default function squareArea(side) {
return side * side;
}Importing Modules
Imports reference exported items by exact name (named) or any name (default). Use relative paths like ./math.js (same folder) or ../utils.js (parent folder).
Import syntax
1. Named: import { PI, circleArea } from './math.js';
2. Default: import area from './math.js';
3. Alias: import { PI as piValue } from './math.js';
4. Multiple: import squareArea, { PI } from './math.js';
Browser usage (add to HTML)
<script type="module" src="app.js"></script>
Pro Tip: Always use modules in production—your code becomes importable by frameworks automatically.
Promises: Handling Asynchronous Code
Promises represent eventual completion (or failure) of async operations like API calls or timers.
They chain .then() for success and .catch() for errors, avoiding callback hell.
Promise Lifecycle and Chaining
A promise starts as pending, becomes fulfilled (resolved) or rejected. Chain multiple operations without nesting.
Basic lifecycle
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => resolve("Success!"), 1000);
});
myPromise
.then(result => console.log(result))
.catch(error => console.error(error));Real example - Fetch user data
Create promise with fetch()
.then() parse JSON
.then() display data
.catch() handle network errors
Chaining multiple promises
fetchUser()
.then(user => fetchPosts(user.id))
.then(posts => displayPosts(posts))
.catch(handleError);Common Promise Methods
Master these static methods for complex flows

Promise.all() practical
Promise.all([fetchUsers(), fetchPosts()])
.then(([users, posts]) => renderDashboard(users, posts));Async/await is syntactic sugar over promises, making asynchronous code read like synchronous. Use async functions and await keyword for cleaner error handling with try/catch.
Writing Async Functions
Mark functions async—they always return promises. await pauses execution until promise settles.
Basic conversion
// Promise chaining
fetch('/api/user').then(r => r.json()).then(display);
// Async/await equivalent
async function getUser() {
const response = await fetch('/api/user');
const user = await response.json();
display(user);
}Error handling
async function safeFetch() {
try {
const data = await fetchData();
return data;
} catch (error) {
console.error('Fetch failed:', error);
return null;
}
}Parallel vs Sequential Awaits
Avoid sequential blocking—use Promise.all():
| Pattern | Speed | Use Case |
|---|---|---|
await p1; await p2; | Sequential | Dependencies |
await Promise.all([p1, p2]); | Parallel | Independent |
Dashboard example
async function loadDashboard() {
const [users, posts, stats] = await Promise.all([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/stats')
]);
renderAll(users.json(), posts.json(), stats.json());
}Fetch API: Modern AJAX
Fetch API replaces XMLHttpRequest with a promise-based interface for HTTP requests. Native to browsers since 2015, it supports modern features like streaming and CORS.
Making Basic Requests
Fetch returns a Response object—always check ok status and parse JSON manually.
GET request
async function fetchUsers() {
try {
const response = await fetch('https://api.github.com/users');
if (!response.ok) throw new Error('Network error');
return await response.json();
} catch (error) {
console.error(error);
}
}POST request with JSON:
async function createUser(userData) {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
return response.json();
}Advanced Fetch Features
Handle real-world scenarios:

Complete example with Abort
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
fetch('/api/slow', { signal: controller.signal })
.then(r => r.json())
.catch(err => {
if (err.name === 'AbortError') console.log('Request timeout');
});