In this note we will look at the pattern used to fetch data, and some idiomatic ways to accomplish the task of fetching some data from an endpoint. Keep in mind that this is written entirely from a Svelte perspective, and a knowledge of Reactivity and Logic is assumed. I am also assuming that we’re not using SvelteKit or server-side rendering (SSR). That changes the equation by a bit. So only vanilla Svelte is covered here.
Fetching is when we use a function to send an HTTP GET request to an endpoint to fetch some data. The native fetch API can be used to send all kinds of requests and headers, but a common use case is to GET some data from an endpoint. As such, that is what I have used in these examples.
The idiomatic to fetch data without SSR is to use {#await} blocks. They can do error handling too, using . You could add a “loading” component inside the await block and wrap everything you want to show around then.
That is, we’re dealing with a pattern like this:
{#await promise}
<!-- promise is pending -->
<p>waiting for the promise to resolve...</p>
{:then value}
<!-- promise was fulfilled or not a Promise -->
<p>The value is {value}</p>
{:catch error}
<!-- promise was rejected -->
<p>Something went wrong: {error.message}</p>
{/await}Now the fetch API returns a Response on sending a request. When we perform .json() on the Response object, we still get a Promise. Thus, ideally, the pattern would look something like this:
<script>
async function fetchData() {
let response = await fetch('https://api.example.com/'); // fetch returns a promise object, await pauses the function and waits for the promise to resolve and assigns the value to response
const data = await response.json(); // we await for another promise object (response.json()) to resolve and return another promise and assign the value to data
return data; // data is the resolved JSON, but because this is an async function, it automatically wraps the return value in a new Promise. You need to await this promise (or use a .then()) to unwrap it!
// return await response.json(); is also totally valid - it too returns a promise.
}
</script>
{#await fetchData()}
<!-- promise is pending -->
<p>a loading message while the promise resolves...</p>
{:then result}
<!-- promise was fulfilled or not a Promise -->
<p>The JSON object is {JSON.stringify(result)}</p>
{:catch error}
<!-- promise was rejected -->
<p>Something went wrong: {error.message}</p>
{/await}If the fetchData() function had a reactive parameter, say URL that updates everytime the user types in something in a search bar - that would be registered as a reactive dependency by Svelte and on every keystroke, a new request would fire (as we’re updating URL on every keystroke). See below on how to resolve this issue. Internally, Svelte still uses $effects for await blocks.
What if you want to access result in the <script>? To resolve this scope issue you need to create a variable for the Promise object returned by the function and hand over this variable to {#await}. Separately, you can use a standard promise resolution method, like await inside an async function or simply, .then() to hand over the resolved value to some other variable when the final resolution is done. That would be something like:
<script>
let finalData; // this is where I want to save the final JSON
async function fetchData() {
let response = await fetch('https://api.example.com/'); // fetch returns a promise object, await pauses the function and waits for the promise to resolve and assigns the value to response
const data = await response.json(); // we await for another promise object (response.json()) to resolve and return another promise and assign the value to data
return data; // data is the resolved JSON, but because this is an async function, it automatically wraps the return value in a new Promise. You need to await this promise (or use a .then()) to unwrap it!
// return await response.json(); is also totally valid - it too returns a promise.
}
const fetchPromise = fetchData(); // this needs to be awaited
// do this AFTER the promise resolves
fetchPromise.then( (result) => {
finalData = result; // save the final resolved value in data
console.log(finalData); // log it to the console for debugging
})
// this is also a valid way to resolve the promise:
// const temp = async () => {
// finalData = await fetchData();
// console.log(finalData);
// }
// temp(); // don't forget to actually call the async function!
</script>
{#await fetchPromise}
<!-- promise is pending -->
<p>a loading message while the promise resolves...</p>
{:then result}
<!-- promise was fulfilled or not a Promise -->
<p>The JSON object is {JSON.stringify(result)}</p>
{:catch error}
<!-- promise was rejected -->
<p>Something went wrong: {error.message}</p>
{/await}This way, you can share an asynchronous request between your markup and your script.
A note on side effects
Note that a pattern like
<script lang="ts">
$effect( () => {
async function nonIdiomaticFetch(url: string) {
/*
* This function returns nothing. It's an async function whose only job is to fetch a url and assign the JSON value to a variable.
* An async function always returns a promise. A common pitfall I have seen myself falling for is to return await response.json() and expect that it would return a nice JSON object even if the outside function isn't marked async. It does not, an async function can only ever return a promise.
* What you could do to resolve this (pun intended) is to use a .then((result) => { // do something with the result }) which could be used to assign something to the returned JSON object.
* Another approach, the one that I've opted for here, is to assign the value after the await, so that this is done when the value is actually computed (await makes the promise resolve later).
*/
const response: Response = await fetch(url);
if (!response.ok) {
throw new Error('There was an error in fetching the JSON data from the endpoint.')
}
else {
finalData = await response.json(); // this returns a resolved promise (the JSON object)
console.log(finalData);
}
}
// actually do stuff.
nonIdiomaticFetch(API_URL);
// hitEndpoint(API_URL).then( (result) => {
// // we use .then() here to "resolve" the promise
// // but we could sure as hell use an async/await function and call it here
// // now that the promise is resolved, we can log this to the console for debugging
// finalData = result;
// console.log(finalData);
// });
});
</script>..is a legal way to fetch data, but is ineffective, and non-idiomatic. The Svelte-native {#await} blocks handle everything for you, even situations when you want to hand over the fetched data to your <script>.
A Promise as state
Now consider a situation where you need to fetch data again and where API_URL is state. And let’s imagine that we’re implementing a feature where we take the user’s input in a search bar and change API_URL based on what that input is. Thus, we will make API_URL state. Now if we want to render the components, we could declare a $derived value that is a Promise, which derives from the Promise returned when fetch(API_URL) is called, or the moment we call fetch (note that API_URL is state, that’s the reason the Promise can be a derived value).
Now you could use that promise inside an await block and let Svelte handle how the UI transitions between loading, success, and error phases. That is a better pattern - making the fetched Promise a $state or $derived and using it in the {#await} block.
Now what if we run into a situation where the user types in c, and then a and then t for cat and we fire off 4 separate fetch requests instead of 1? Here, you wouldn’t use a derived promise but instead have the promise as a regular state and create a debounced event handler to handle the timers; when the user finally pauses we create a new (final) fetch promise and assign it to state.