Many WordPress features depend on work that finishes after the page has loaded: live search, product filters, admin dashboards, form submissions, and block editor tools all need to request or process data without interrupting the interface.
This guide covers the practical parts of asynchronous JavaScript that WordPress developers use most often, including promises, async/await, the Fetch API, REST authentication, concurrent requests, cancellation, and error handling. The examples can be adapted for custom plugins, themes, WooCommerce features, and small front-end applications.
If you are building your JavaScript foundation, begin with this practical roadmap for learning WordPress coding.
What asynchronous JavaScript means
Synchronous code runs in sequence. When it reaches a slow operation, such as a server request, the next step cannot run until that operation finishes.
Asynchronous code starts the operation and lets the browser continue handling other work. JavaScript then responds when the operation completes. This model is important in WordPress because REST API requests, AJAX actions, and third-party integrations all involve waiting for a response.
Promises in simple terms
A promise represents a result that will be available later. It can be pending, fulfilled, or rejected. The then() method handles a successful result, while catch() handles an error.
fetch('/wp-json/wp/v2/posts?per_page=5')
.then(response => response.json())
.then(posts => {
console.log(posts);
})
.catch(error => {
console.error('Request failed:', error);
});
Promise chains are useful, but several dependent steps can become difficult to follow. For most new WordPress code, async and await make the control flow easier to read.
Using async and await
An async function always returns a promise. Within that function, await waits for a promise to settle before continuing that function. It does not freeze the entire browser.
async function loadPosts() {
const response = await fetch('/wp-json/wp/v2/posts?per_page=5');
return response.json();
}
loadPosts()
.then(posts => console.log(posts))
.catch(error => console.error(error));
Fetch does not treat every HTTP error as a rejected promise. A request can complete with a 403, 404, or 500 response, so production code should check response.ok before parsing the body.
async function loadPosts() {
const response = await fetch('/wp-json/wp/v2/posts?per_page=5');
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
}
async function start() {
try {
const posts = await loadPosts();
console.log(posts);
} catch (error) {
console.error('Could not load posts:', error);
}
}
start();
Calling the WordPress REST API with Fetch
The REST API provides a straightforward way for browser code to request WordPress data. A plugin can expose a custom route, or JavaScript can call a core or WooCommerce endpoint.
async function getProducts() {
const response = await fetch('/wp-json/wc/store/v1/products?per_page=6');
if (!response.ok) {
throw new Error('Unable to load products');
}
return response.json();
}
async function renderProducts() {
const container = document.querySelector('#product-list');
if (!container) return;
try {
const products = await getProducts();
products.forEach(product => {
const article = document.createElement('article');
const heading = document.createElement('h3');
heading.textContent = product.name;
article.appendChild(heading);
container.appendChild(article);
});
} catch (error) {
container.textContent = 'Products could not be loaded.';
console.error(error);
}
}
renderProducts();
Use DOM methods such as textContent when inserting server-provided values where possible. If you build HTML strings, escape values correctly. Data from your own site should not automatically be treated as safe for HTML insertion.
Authenticated WordPress requests
Public REST endpoints may not require authentication. Requests that create, edit, or delete content do. In a logged-in WordPress screen, REST requests commonly include a WordPress nonce.
A plugin or theme can pass the nonce and other configuration to JavaScript with wp_localize_script() or wp_add_inline_script(). A simplified update request looks like this:
async function updatePost(postId, title) {
const response = await fetch(`/wp-json/wp/v2/posts/${postId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': window.myWpData.nonce
},
body: JSON.stringify({ title })
});
if (!response.ok) {
const message = await response.text();
throw new Error(message || 'Update failed');
}
return response.json();
}
A nonce is not a substitute for authorization. The server-side callback must still check the current user’s capabilities, validate the input, and handle stored or returned data safely. See this practical guide to API authentication in WordPress for additional context.
Handling multiple requests efficiently
When requests are independent, Promise.all() lets them start together instead of waiting for each one in turn.
async function loadDashboard() {
const [ordersResponse, settingsResponse] = await Promise.all([
fetch('/wp-json/my-plugin/v1/orders'),
fetch('/wp-json/my-plugin/v1/settings')
]);
if (!ordersResponse.ok || !settingsResponse.ok) {
throw new Error('Dashboard data could not be loaded');
}
const [orders, settings] = await Promise.all([
ordersResponse.json(),
settingsResponse.json()
]);
return { orders, settings };
}
Use concurrent requests only when they are genuinely independent. If the second request needs an identifier returned by the first, sequential await calls are the correct approach.
Preventing common asynchronous JavaScript bugs
Remember that async functions return promises
Calling an async function without await gives you a promise, not the resolved value. This is a frequent source of errors when code tries to read properties from the wrong object.
Prevent duplicate submissions
Save, login, checkout, and search controls should not send a new request for every click. Disable the control while the request is active and restore it in a finally block, so both success and failure leave the interface usable.
async function saveSettings(button) {
button.disabled = true;
try {
const response = await fetch('/wp-json/my-plugin/v1/settings', {
method: 'POST'
});
if (!response.ok) {
throw new Error('Saving settings failed');
}
} catch (error) {
console.error(error);
} finally {
button.disabled = false;
}
}
Cancel obsolete searches
Live search can create several requests while someone is typing. An AbortController can cancel the previous request before starting another, reducing unnecessary work and preventing older results from replacing newer ones.
let controller;
async function searchPosts(term) {
controller?.abort();
controller = new AbortController();
const response = await fetch(
`/wp-json/wp/v2/search?search=${encodeURIComponent(term)}`,
{ signal: controller.signal }
);
if (!response.ok) {
throw new Error('Search failed');
}
return response.json();
}
Aborted requests produce an error, so calling code should decide whether to ignore expected abort errors or display a message for genuine failures.
WordPress performance and debugging checklist
- Enqueue scripts only on pages that need them.
- Use pagination instead of requesting unnecessarily large datasets.
- Show a loading state and a useful failure message.
- Check the response status before parsing the body.
- Use the browser Network panel to inspect URLs, payloads, status codes, and timing.
- Test logged-out and logged-in behavior separately.
- Keep permissions, validation, and sanitization on the server.
Asynchronous JavaScript does not remove server-side security requirements. For database-backed features, review prepared queries and safe database patterns in this guide to using $wpdb safely.
When a WordPress JavaScript project needs more planning
Async code becomes harder to maintain when it coordinates WooCommerce data, custom REST routes, authentication, caching, optimistic updates, or third-party APIs. A feature can appear to work while still allowing race conditions, duplicate submissions, permission errors, or slow admin screens.
For a custom plugin, REST integration, WooCommerce workflow, or JavaScript performance review, plan the request flow before writing the interface. Define the server contract, validate permissions on the server, handle loading and failure states, and test with realistic logged-in and logged-out conditions.
Frequently asked questions
Is async/await better than promises?
It is often easier to read and maintain when a function has several dependent steps and error conditions. Both approaches use promises underneath, so the choice is mainly about clarity and project conventions.
Should I use the REST API or admin-ajax.php?
REST routes are often a cleaner choice for structured modern integrations. Existing plugins may still use admin-ajax.php, so compatibility and the requirements of the project should guide the decision.
Why does Fetch not reject on a 404?
Fetch generally rejects for network-level failures, not ordinary HTTP error statuses. Check response.ok and throw an application error when the status is unsuccessful.
Can I use async JavaScript in a WordPress theme?
Yes. Enqueue the script properly, pass required configuration from PHP, and make sure the code handles missing elements, unavailable endpoints, authentication failures, and empty responses.
Conclusion
Modern asynchronous JavaScript gives WordPress interfaces a way to request and update data without unnecessary page reloads. Start with Fetch and async/await, check response statuses, protect authenticated routes, prevent duplicate requests, and test both browser and server behavior. Those habits make custom WordPress features more predictable, secure, and maintainable.
