jQuery’s $.ajax() has been the standard tool for asynchronous HTTP requests in WordPress themes and plugins for over a decade. It works, it is well-documented, and almost every WordPress developer knows it by heart. But it comes with a cost: a hard dependency on jQuery, which means an extra network request (or a larger bundle), deferred loading headaches, and code that feels increasingly out of step with modern JavaScript. The native Fetch API has been available in all browsers without a polyfill since 2017. It is promise-based, requires zero dependencies, supports async/await cleanly, and works with exactly the same WordPress AJAX endpoint — /wp-admin/admin-ajax.php — that $.ajax() targets. The PHP handler code does not change at all: you still register your action with add_action('wp_ajax_*'), you still call check_ajax_referer(), and you still return JSON with wp_send_json_success(). Only the JavaScript side changes. This article shows a direct side-by-side comparison, an async/await pattern, and the one important edge case: when you send JSON (rather than form-encoded data) in the request body, nonce verification needs a small adjustment because $_POST is empty for JSON requests.
Problem: Your WordPress theme or plugin uses jQuery's $.ajax() for every AJAX request. You want to remove the jQuery dependency or modernise the code to native JavaScript without rewriting the PHP handler.
Solution: Replace $.ajax() with the native fetch(). The WordPress AJAX endpoint accepts standard HTTP POST with application/x-www-form-urlencoded body — no jQuery required. The PHP-side code stays identical.
Side-by-side comparison — the same WordPress AJAX request, two ways:
// Before: jQuery $.ajax
jQuery.ajax( {
url: myData.ajaxUrl,
method: 'POST',
data: { action: 'get_posts', nonce: myData.nonce },
success: function ( response ) { console.log( response ); },
error: function ( xhr ) { console.error( xhr.responseText ); },
} );
// After: native Fetch API
fetch( myData.ajaxUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams( { action: 'get_posts', nonce: myData.nonce } ),
} )
.then( function ( response ) {
if ( ! response.ok ) { throw new Error( 'Network error: ' + response.status ); }
return response.json();
} )
.then( function ( data ) { console.log( data ); } )
.catch( function ( err ) { console.error( err ); } );
With async/await the same call is even cleaner:
async function loadPosts() {
try {
const response = await fetch( myData.ajaxUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams( { action: 'get_posts', nonce: myData.nonce } ),
} );
if ( ! response.ok ) { throw new Error( 'HTTP ' + response.status ); }
const data = await response.json();
console.log( data );
} catch ( error ) {
console.error( 'Request failed:', error );
}
}
The PHP handler is unchanged regardless of which JS approach you use:
<?php
add_action( 'wp_ajax_get_posts', 'ajax_get_posts_handler' );
add_action( 'wp_ajax_nopriv_get_posts', 'ajax_get_posts_handler' );
function ajax_get_posts_handler() {
check_ajax_referer( 'my_nonce_action', 'nonce' );
$posts = get_posts( [
'post_type' => 'post',
'posts_per_page' => 5,
'post_status' => 'publish',
] );
wp_send_json_success( array_map( function ( $post ) {
return [
'id' => $post->ID,
'title' => get_the_title( $post->ID ),
'url' => get_permalink( $post->ID ),
];
}, $posts ) );
}
If you send JSON in the body instead of form-encoded data, $_POST will be empty and check_ajax_referer() will fail. Verify the nonce manually from php://input:
<?php
add_action( 'wp_ajax_my_action', 'my_json_handler' );
function my_json_handler() {
$data = json_decode( file_get_contents( 'php://input' ), true );
if ( empty( $data['nonce'] ) || ! wp_verify_nonce( $data['nonce'], 'my_nonce_action' ) ) {
wp_send_json_error( 'Invalid nonce', 403 );
}
$ids = array_map( 'absint', $data['ids'] ?? [] );
wp_send_json_success( $ids );
}
NOTE: fetch() does not reject the promise on HTTP error status codes (4xx, 5xx) — only on network failures. Always check response.ok or response.status explicitly, otherwise a 403 or 500 response will silently pass to the .then() handler and response.json() will attempt to parse an error HTML page.