Fetching Data with async/await
You have the syntax. Now apply it. This lesson writes the complete api.js module for BudgetBuddy, combining everything from Module 05 and Module 06 into production-ready async code.
The complete api.js
Section titled “The complete api.js”const BASE_URL = 'https://open.er-api.com/v6/latest';const SUPPORTED_CURRENCIES = ['USD', 'EUR', 'GBP', 'CAD', 'JPY', 'AUD', 'CHF', 'MXN'];
let cachedRates = null;let lastFetchTime = null;const CACHE_DURATION_MS = 60 * 60 * 1000; // 1 hour
export async function fetchRates(baseCurrency = 'USD') { const now = Date.now(); if (cachedRates && lastFetchTime && now - lastFetchTime < CACHE_DURATION_MS) { return cachedRates; }
const response = await fetch(`${BASE_URL}/${baseCurrency}`); if (!response.ok) { throw new Error(`Exchange rate API returned ${response.status}`); }
const data = await response.json(); if (data.result !== 'success') { throw new Error(`API error: ${data['error-type'] ?? 'unknown'}`); }
cachedRates = filterRates(data.rates); lastFetchTime = now; return cachedRates;}
function filterRates(rates) { return SUPPORTED_CURRENCIES.reduce((acc, code) => { if (rates[code] !== undefined) acc[code] = rates[code]; return acc; }, {});}
export function convertAmount(amount, fromCurrency, toCurrency, rates) { if (fromCurrency === toCurrency) return amount; const fromRate = rates[fromCurrency]; const toRate = rates[toCurrency]; if (!fromRate || !toRate) throw new Error(`Unknown currency: ${fromCurrency} or ${toCurrency}`); return (amount / fromRate) * toRate;}Key improvements over the Module 05 version:
- Cache expires after 1 hour, so rates stay relatively fresh
filterRatesusesreducefrom Module 01 to extract only supported currenciesconvertAmountis exported as a named utility alongsidefetchRates- The function throws — callers handle the error in context
Calling fetchRates in main.js
Section titled “Calling fetchRates in main.js”import Expense from './expense.js';import { saveExpenses, loadExpenses } from './storage.js';import { fetchRates, convertAmount } from './api.js';import { renderExpenses } from './ui.js';
let expenses = loadExpenses();let rates = { USD: 1 }; // fallback until real rates load
async function init() { try { rates = await fetchRates(); } catch (error) { console.warn('Using USD-only rates:', error.message); } renderExpenses(expenses, rates);}
init();init is the entry point. It tries to fetch rates — if it fails, it silently degrades to USD-only mode. Either way, the app renders.
Responding to user actions asynchronously
Section titled “Responding to user actions asynchronously”Event handlers can be async too:
document.querySelector('#currency-select').addEventListener('change', async event => { const currency = event.target.value; try { const newRates = await fetchRates(currency); rates = newRates; renderExpenses(expenses, rates); } catch (error) { showErrorBanner(`Could not load ${currency} rates.`); }});An async event handler works exactly like any other async function — just be aware that you cannot await it from the outside, so any unhandled errors will be unhandled rejections.
Awaiting in loops
Section titled “Awaiting in loops”await inside a for...of loop runs each iteration sequentially — the next iteration waits for the current one:
const currencies = ['EUR', 'GBP', 'JPY'];
for (const currency of currencies) { const rates = await fetchRates(currency); console.log(`${currency}:`, rates);}// EUR rates, then GBP rates, then JPY rates — in orderThis makes three sequential network requests. For parallel requests, use Promise.all (next lesson).
Exercise
Section titled “Exercise”- Implement the full
api.jsmodule as shown above. - Call
fetchRates()frommain.jsinside anasync init()function with graceful fallback. - Log the supported currencies and their current rates.
- Add an event listener on a
<select>element that callsfetchRates(selectedCurrency)and logs the result. (No UI yet — just verify the async event handler works.)
asyncfunctions can be exported directly as module functions — callers use them like any other function.- Cache fetch results to avoid hammering the API on every interaction.
- Event listeners can be
async— just make sure errors are caught inside the handler. awaitin afor...ofloop runs iterations sequentially. UsePromise.allfor parallel.- Module-level
let rates = { USD: 1 }provides a safe default while the real rates load.