Understanding async() / asyncState() - A Beginner's Guide
Quick Start (30 seconds)
Need to manage async data with loading and error states? Here's how:
// Create async state
const userData = asyncState(null);
// Auto-update UI based on state
effect(() => {
if (userData.loading) {
document.getElementById('status').textContent = 'Loading...';
} else if (userData.error) {
document.getElementById('status').textContent = 'Error: ' + userData.error;
} else if (userData.data) {
document.getElementById('status').textContent = 'Loaded!';
displayUser(userData.data);
}
});
// Execute async operation - loading/error managed automatically!
await execute(userData, async () => {
const response = await fetch('/api/user');
return response.json();
});That's it! The asyncState() function creates reactive state that automatically manages loading, data, and error states for async operations!
What is async()?
async() (also called asyncState()) is a specialized function for managing asynchronous operations with built-in loading and error state management. It takes care of the common async patterns so you don't have to.
A reactive async state:
- Manages data, loading, and error states automatically
- Provides computed properties (isSuccess, isError)
- Handles async operation execution
- Supports request cancellation
- Tracks operation status
Think of it as a smart async operation manager - it handles all the tedious async state management (loading flags, error handling, data storage) automatically.
Syntax
// Using the shortcut
asyncState(initialValue)
// Using the full namespace
ReactiveUtils.async(initialValue)Both styles are valid! Choose whichever you prefer:
- Shortcut style (
asyncState()) - Clean and concise - Namespace style (
ReactiveUtils.async()) - Explicit and clear
Parameters:
initialValue- Initial value for the data property (defaults tonull)
Returns:
- A reactive async state object with data, loading, error properties and methods
Why Does This Exist?
Two Approaches to Async State Management
The Reactive library offers flexible ways to handle asynchronous operations, each suited to different control needs.
Manual Async State Control
When you need explicit control over each step of the async flow and want to customize loading/error handling:
// Explicit async state management
const apiState = state({
data: null,
loading: false,
error: null
});
async function fetchUser() {
// Explicitly manage loading state
apiState.loading = true;
apiState.error = null;
try {
const response = await fetch('/api/user');
const data = await response.json();
// Custom success handling
apiState.data = data;
} catch (err) {
// Custom error handling
apiState.error = err.message;
} finally {
// Explicitly clear loading
apiState.loading = false;
}
}This approach is great when you need: ✅ Full control over each state transition ✅ Custom logic during loading/success/error phases ✅ Specific error handling patterns ✅ Integration with existing async patterns
When Standardized Async Patterns Fit Your Needs
In scenarios where you want consistent async state management with automatic loading/error/data handling, asyncState() provides a more direct approach:
// Structured async state with automatic management
const userData = asyncState(null);
// Execute with automatic state handling
await execute(userData, async () => {
const response = await fetch('/api/user');
return response.json();
});
// Loading, error, data all managed automaticallyThis method is especially useful when:
asyncState() Flow:
┌──────────────────┐
│ execute() │
└────────┬─────────┘
│
▼
Automatic management:
• loading = true
• error cleared
• runs function
• updates data/error
• loading = false
│
▼
✅ Consistent patternWhere asyncState() shines: ✅ Standardized patterns - Same structure for all async operations ✅ Automatic state tracking - Loading, error, data managed for you ✅ Computed helpers - isSuccess, isError, isIdle built-in ✅ Request cancellation - Built-in AbortSignal support ✅ Reduced boilerplate - No manual try/catch/finally needed ✅ Consistent error handling - Same pattern across your app
The Choice is Yours:
- Use manual async state when you need custom control over state transitions
- Use
asyncState()when you want standardized async patterns - Both approaches work seamlessly with reactive state
Benefits of the asyncState approach: ✅ Automatic lifecycle - Loading, success, and error states managed ✅ Built-in helpers - Computed properties for common checks ✅ Cancellation support - Easy request cancellation with AbortSignal ✅ Less code - Focus on the async logic, not the state management ✅ Consistent API - Same pattern for all async operations in your app
Mental Model
Think of asyncState() like a package delivery tracker:
Manual Async State (DIY Tracking):
┌──────────────────────┐
│ You track manually: │
│ ✓ Is it shipped? │ ← Track manually
│ ✓ Where is it? │ ← Track manually
│ ✓ Any errors? │ ← Track manually
│ ✓ Has it arrived? │ ← Track manually
└──────────────────────┘
You do everything!
Async State (Smart Tracker):
┌──────────────────────────────────┐
│ Package Tracker │
│ ┌────────────────────┐ │
│ │ Status: In Transit │ ✓ │
│ │ Location: City X │ ✓ │
│ │ Errors: None │ ✓ │
│ │ Delivered: No │ ✓ │
│ └────────────────────┘ │
│ │
│ Automatic Updates: │
│ ✓ Shipped → loading = true │
│ ✓ Delivered → data = package │
│ ✓ Lost → error = message │
│ ✓ Done → loading = false │
└──────────────────────────────────┘
│
▼
Everything tracked
automatically!Key Insight: Just like a package tracker that automatically updates as your package moves through different stages, asyncState() automatically updates as your async operation progresses through loading, success, and error states.
How Does It Work?
The Magic: State + Execution Wrapper
When you call asyncState(), here's what happens behind the scenes:
// What you write:
const userData = asyncState(null);
// What actually happens (simplified):
// 1. Create reactive state
const userData = state({
data: null,
loading: false,
error: null
});
// 2. Add computed properties
userData.isSuccess = computed(() =>
!userData.loading && !userData.error && userData.data !== null
);
userData.isError = computed(() =>
!userData.loading && userData.error !== null
);
// 3. Add execution method
// (provided via Module 09: execute(userData, fn))In other words: asyncState() is a package that:
- Creates reactive state with data/loading/error properties
- Adds computed properties for checking status
- Works with
execute()function to run async operations - Manages all state transitions automatically
- Returns a fully-functional async state manager
Under the Hood
asyncState(null)
│
▼
┌───────────────────────┐
│ Step 1: Create State │
│ data, loading, error │
└──────────┬────────────┘
│
▼
┌───────────────────────┐
│ Step 2: Add Computed │
│ isSuccess, isError │
└──────────┬────────────┘
│
▼
┌───────────────────────┐
│ Step 3: Return │
│ Async State Object │
└───────────────────────┘When you execute:
execute(asyncState, fn)
│
▼
┌───────────────────────┐
│ Set loading = true │
│ Set error = null │
└──────────┬────────────┘
│
▼
┌───────────────────────┐
│ Run your async │
│ function │
└──────────┬────────────┘
│
┌──────┴──────┐
│ │
▼ ▼
Success Error
│ │
▼ ▼
Set data Set error
│ │
└──────┬──────┘
│
▼
┌───────────────────────┐
│ Set loading = false │
└───────────────────────┘Basic Usage
Creating Async State
The simplest way to use asyncState():
// Create with null initial value
const userData = asyncState(null);
// Or with a default value
const userData = asyncState({ name: 'Guest' });Executing Async Operations
Use the execute() function (from Module 09) to run async operations:
const userData = asyncState(null);
// Execute an async operation
await execute(userData, async () => {
const response = await fetch('/api/user');
return response.json();
});
// Data is now available
console.log(userData.data);Async State Properties
asyncState.data
The data returned from the async operation:
const userData = asyncState(null);
console.log(userData.data); // null (initial)
await execute(userData, async () => {
return { name: 'John', age: 25 };
});
console.log(userData.data);
// { name: 'John', age: 25 }asyncState.loading
Boolean indicating if operation is in progress:
const userData = asyncState(null);
console.log(userData.loading); // false
const promise = execute(userData, async () => {
await delay(2000);
return { name: 'John' };
});
console.log(userData.loading); // true
await promise;
console.log(userData.loading); // falseasyncState.error
Error message if operation failed:
const userData = asyncState(null);
await execute(userData, async () => {
throw new Error('Network error');
});
console.log(userData.error); // "Network error"asyncState.isSuccess (Computed)
Automatically computed - true if data loaded successfully:
const userData = asyncState(null);
console.log(userData.isSuccess); // false (no data yet)
await execute(userData, async () => {
return { name: 'John' };
});
console.log(userData.isSuccess); // trueFormula: !loading && !error && data !== null
asyncState.isError (Computed)
Automatically computed - true if operation failed:
const userData = asyncState(null);
console.log(userData.isError); // false
await execute(userData, async () => {
throw new Error('Failed');
});
console.log(userData.isError); // trueFormula: !loading && error !== null
Executing Async Operations
execute(asyncState, fn)
Execute an async function and manage state:
const userData = asyncState(null);
await execute(userData, async () => {
// Your async logic here
const response = await fetch('/api/user');
if (!response.ok) {
throw new Error('Failed to fetch');
}
return response.json();
});
// State is automatically managed:
// - loading set to true at start
// - error cleared at start
// - data set on success
// - error set on failure
// - loading set to false at endReturns: Promise that resolves to the result
With Parameters
const userDetails = asyncState(null);
async function fetchUser(userId) {
await execute(userDetails, async () => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
});
}
await fetchUser(123);Enhanced Execute (Module 06)
If Module 06 is loaded, execute() supports AbortSignal:
const userData = asyncState(null);
await execute(userData, async (signal) => {
const response = await fetch('/api/user', { signal });
return response.json();
});
// Can abort:
abort(userData);Handling Loading States
Display Loading Indicator
const posts = asyncState(null);
// Show loading spinner
effect(() => {
const spinner = document.getElementById('spinner');
spinner.style.display = posts.loading ? 'block' : 'none';
});
// Fetch posts
await execute(posts, async () => {
const response = await fetch('/api/posts');
return response.json();
});Disable Buttons During Loading
const userData = asyncState(null);
effect(() => {
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = userData.loading;
submitBtn.textContent = userData.loading ? 'Loading...' : 'Submit';
});Show Different Content Based on State
const articles = asyncState(null);
effect(() => {
const container = document.getElementById('content');
if (articles.loading) {
container.innerHTML = '<div>Loading articles...</div>';
} else if (articles.isSuccess) {
container.innerHTML = articles.data
.map(article => `<div>${article.title}</div>`)
.join('');
}
});Error Handling
Display Error Messages
const userData = asyncState(null);
effect(() => {
const errorDiv = document.getElementById('error');
if (userData.isError) {
errorDiv.textContent = userData.error;
errorDiv.style.display = 'block';
} else {
errorDiv.style.display = 'none';
}
});
// Execute with potential error
await execute(userData, async () => {
const response = await fetch('/api/user');
if (!response.ok) {
throw new Error('Failed to load user');
}
return response.json();
});Retry on Error
const userData = asyncState(null);
async function fetchWithRetry(maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
await execute(userData, async () => {
const response = await fetch('/api/user');
if (!response.ok) throw new Error('Failed');
return response.json();
});
if (userData.isSuccess) break;
// Wait before retry
await delay(1000 * (i + 1));
}
}Custom Error Handling
const userData = asyncState(null);
await execute(userData, async () => {
try {
const response = await fetch('/api/user');
return response.json();
} catch (err) {
// Transform error before it's stored
throw new Error(`Network error: ${err.message}`);
}
});Aborting Requests
abort(asyncState) (Module 09)
Abort an in-progress operation:
const searchResults = asyncState(null);
// Start long search
execute(searchResults, async (signal) => {
const response = await fetch('/api/search?q=test', { signal });
return response.json();
});
// User changes search query - abort previous request
abort(searchResults);
// Start new search
execute(searchResults, async (signal) => {
const response = await fetch('/api/search?q=new', { signal });
return response.json();
});Auto-Abort on New Request
const searchResults = asyncState(null);
async function search(query) {
// Abort previous search
abort(searchResults);
// Start new search
await execute(searchResults, async (signal) => {
const response = await fetch(`/api/search?q=${query}`, { signal });
return response.json();
});
}
// Only the last search completes
search('test1');
search('test2');
search('test3'); // Previous two are abortedResetting and Refetching
reset(asyncState) (Module 09)
Reset state to initial values:
const userData = asyncState(null);
// Fetch data
await execute(userData, async () => {
return { name: 'John' };
});
console.log(userData.data); // { name: 'John' }
// Reset
reset(userData);
console.log(userData.data); // null
console.log(userData.loading); // false
console.log(userData.error); // nullrefetch(asyncState) (Module 09)
Re-run the last async function:
const userData = asyncState(null);
// First fetch
await execute(userData, async () => {
const response = await fetch('/api/user');
return response.json();
});
// Later, refetch with the same function
await refetch(userData);Note: refetch() only works if the state was created with Module 06 (Enhanced) which stores the last function.
Working with Effects
Async state automatically triggers effects when it changes:
const userData = asyncState(null);
// Effect runs whenever any property changes
effect(() => {
console.log('Loading:', userData.loading);
console.log('Data:', userData.data);
console.log('Error:', userData.error);
});
// Execute - effect re-runs multiple times:
// 1. When loading becomes true
// 2. When data is set and loading becomes false
await execute(userData, async () => {
return { name: 'John' };
});Separate Effects for Each State
const userData = asyncState(null);
// Effect for loading
effect(() => {
if (userData.loading) {
showSpinner();
} else {
hideSpinner();
}
});
// Effect for success
effect(() => {
if (userData.isSuccess) {
displayUser(userData.data);
}
});
// Effect for error
effect(() => {
if (userData.isError) {
displayError(userData.error);
}
});Common Patterns
Pattern: Data Fetching
const articles = asyncState(null);
// Fetch on page load
document.addEventListener('DOMContentLoaded', async () => {
await execute(articles, async () => {
const response = await fetch('/api/articles');
return response.json();
});
});
// Display based on state
effect(() => {
const container = document.getElementById('articles');
if (articles.loading) {
container.innerHTML = '<div class="spinner">Loading...</div>';
} else if (articles.isError) {
container.innerHTML = `<div class="error">${articles.error}</div>`;
} else if (articles.isSuccess) {
container.innerHTML = articles.data
.map(article => `
<article>
<h2>${article.title}</h2>
<p>${article.excerpt}</p>
</article>
`)
.join('');
}
});Pattern: Search with Debounce
const searchResults = asyncState(null);
const searchQuery = ref('');
let searchTimer;
// Watch search query
effect(() => {
const query = searchQuery.value;
clearTimeout(searchTimer);
if (!query) {
reset(searchResults);
return;
}
// Debounce search
searchTimer = setTimeout(async () => {
await execute(searchResults, async (signal) => {
const response = await fetch(
`/api/search?q=${encodeURIComponent(query)}`,
{ signal }
);
return response.json();
});
}, 300);
});
// Connect to input
document.getElementById('search').addEventListener('input', (e) => {
searchQuery.value = e.target.value;
});Pattern: Pagination
const articles = asyncState(null);
const currentPage = ref(1);
// Fetch when page changes
effect(() => {
const page = currentPage.value;
execute(articles, async () => {
const response = await fetch(`/api/articles?page=${page}`);
return response.json();
});
});
function nextPage() {
currentPage.value++;
}
function prevPage() {
if (currentPage.value > 1) {
currentPage.value--;
}
}Pattern: Dependent Requests
const userId = ref(null);
const userDetails = asyncState(null);
const userPosts = asyncState(null);
// Fetch user details when userId changes
effect(() => {
const id = userId.value;
if (!id) {
reset(userDetails);
reset(userPosts);
return;
}
execute(userDetails, async () => {
const response = await fetch(`/api/users/${id}`);
return response.json();
});
});
// Fetch user posts when userDetails loads successfully
effect(() => {
if (userDetails.isSuccess) {
const id = userDetails.data.id;
execute(userPosts, async () => {
const response = await fetch(`/api/users/${id}/posts`);
return response.json();
});
}
});Pattern: Infinite Scroll
const articles = asyncState([]);
const page = ref(1);
const hasMore = ref(true);
async function loadMore() {
if (!hasMore.value || articles.loading) return;
await execute(articles, async () => {
const response = await fetch(`/api/articles?page=${page.value}`);
const newArticles = await response.json();
// Append to existing data
const currentData = articles.data || [];
const combined = [...currentData, ...newArticles];
// Check if more pages
hasMore.value = newArticles.length > 0;
page.value++;
return combined;
});
}
// Trigger on scroll
window.addEventListener('scroll', () => {
const bottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 100;
if (bottom) {
loadMore();
}
});Common Pitfalls
Pitfall #1: Not Awaiting execute()
❌ Wrong:
execute(userData, async () => {
return await fetchUser();
}); // Not awaited
// Code continues immediately
console.log(userData.data); // Still null!✅ Correct:
await execute(userData, async () => {
return await fetchUser();
});
// Now data is available
console.log(userData.data);Pitfall #2: Forgetting to Return Data
❌ Wrong:
await execute(userData, async () => {
const response = await fetch('/api/user');
const data = await response.json();
// Forgot to return!
});
console.log(userData.data); // undefined✅ Correct:
await execute(userData, async () => {
const response = await fetch('/api/user');
return response.json(); // Return the data
});
console.log(userData.data); // Data is setPitfall #3: Checking loading Instead of isSuccess
❌ Wrong:
effect(() => {
// This shows "No data" while loading!
if (!userData.loading && !userData.data) {
console.log('No data');
}
});✅ Correct:
effect(() => {
// Use isSuccess to check if data loaded
if (userData.isSuccess) {
displayData(userData.data);
} else if (userData.isError) {
displayError(userData.error);
} else if (userData.loading) {
showSpinner();
}
});Pitfall #4: Not Handling Errors
❌ Wrong:
await execute(userData, async () => {
const response = await fetch('/api/user');
// If fetch fails, error is caught but not shown
return response.json();
});
// User sees nothing!✅ Correct:
await execute(userData, async () => {
const response = await fetch('/api/user');
if (!response.ok) {
throw new Error('Failed to fetch user');
}
return response.json();
});
// Show error to user
if (userData.isError) {
alert(userData.error);
}Pitfall #5: Modifying data Directly
❌ Wrong:
// Modifying data doesn't trigger loading/error reset
userData.data = newData;✅ Correct:
// Always use execute() to update data
await execute(userData, async () => {
return newData;
});
// Or use reset() then execute()
reset(userData);
await execute(userData, async () => {
return await fetchNewData();
});Summary
What is asyncState()?
asyncState() creates reactive async state with built-in loading and error management. It automatically handles the common async operation patterns.
Why use asyncState() instead of manual state management?
- Automatic loading state management
- Automatic error handling
- Computed properties (isSuccess, isError)
- Less boilerplate (80% code reduction!)
- Request cancellation support
- Standard async patterns handled for you
Key Points to Remember:
1️⃣ Use execute() - Don't modify properties directly 2️⃣ Always return data - The returned value becomes the data 3️⃣ Check isSuccess/isError - Use computed properties for state 4️⃣ Await execute() - Always await to get result 5️⃣ Handle errors - Show errors to users
Mental Model: Think of asyncState() as a package delivery tracker - it automatically updates as your async operation progresses through loading, success, and error states.
Quick Reference:
// Create
const userData = asyncState(null);
// Properties
userData.data // The loaded data
userData.loading // Boolean: is loading
userData.error // Error message if failed
userData.isSuccess // Computed: loaded successfully
userData.isError // Computed: has error
// Execute (Module 09)
await execute(userData, async () => {
const response = await fetch('/api/data');
return response.json();
});
// With AbortSignal (Module 06)
await execute(userData, async (signal) => {
const response = await fetch('/api/data', { signal });
return response.json();
});
// Abort (Module 09)
abort(userData);
// Reset (Module 09)
reset(userData);
// Refetch (Module 09 + Module 06)
await refetch(userData);
// Use in effects
effect(() => {
if (userData.loading) {
showSpinner();
} else if (userData.isSuccess) {
displayData(userData.data);
} else if (userData.isError) {
displayError(userData.error);
}
});Remember: asyncState() is your complete async operation manager. It handles loading, errors, and data automatically, so you can focus on building your UI instead of managing async state!