Fetch will reject your GET if it contains a body (a deliberate maintainer decision), even though it's entirely permissible by HTTP and done by many real-world AJAX APIs. Real AJAX will do what it's supposed to. (The HTTP 1.1 2014 Spec says that including a request body in a GET "might cause some implementations to reject the request." Guess which one!)
Also, advanced features like progress are completely absent from Fetch as well.
However, there are some fantastic libraries like Axios[1], SuperAgent (requires npm), and, yes, jQuery[2], that have really excellent API's (far superior to Fetch), or you could just write your own (or use an LLM) short wrapper around modern AJAX and call it a day. h/t to Claude:
const xhr = ['GET','POST','PUT','PATCH','DELETE'].reduce((x,m) => (x[m.toLowerCase()] =
(u,d,opt={}) => new Promise((r,j) => {
const q = new XMLHttpRequest();
q.open(m,u);
q.responseType = opt.responseType || '';
if(opt.headers) Object.entries(opt.headers).forEach(([k,v]) => q.setRequestHeader(k,v));
if(opt.signal) opt.signal.addEventListener('abort', () => q.abort());
q.withCredentials = opt.credentials === 'include';
q.onload = () => r({
ok: q.status >= 200 && q.status < 300,
status: q.status,
headers: new Headers(q.getAllResponseHeaders()),
text: () => Promise.resolve(q.responseText),
json: () => Promise.resolve(JSON.parse(q.responseText)),
blob: () => Promise.resolve(new Blob([q.response])),
response: q
});
q.onerror = () => j(new TypeError('Network request failed'));
q.send(d instanceof FormData ? d : JSON.stringify(d));
}), x), {});
This gives you xhr methods with a fetch-style API and you can still do all the things that fetch can't (but this won't do real streaming or cache control like Fetch, but it'll do 95% of all common use cases in a tiny bit of code.)Each method listed above returns a Promise that resolves with the XMLHttpRequest object or rejects with the error. So you get both the Promise functionality and full access to the XHR object in the resolution.
Usage:
xhr.post('/api', { data: 123 }, {
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
signal: abortController.signal
})
.then(res => res.json())
.then(data => console.log(data));
For more advanced AJAX stuff, check out the very powerful and flexible Axios library[1].And, if you don't need AJAX but do want some of the features from jQuery (like some of the more unusual selectors) that aren't in Cash (to save bytes!), AJAX (and special effects) is excluded from jQuery Slim which brings the code down to only 69KB[3].
1. Axios https://github.com/axios/axios (41kb)
2. jQuery AJAX https://api.jquery.com/jQuery.ajax/ (87kb but includes ALL of jquery!)