Skip to content

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.

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
  • filterRates uses reduce from Module 01 to extract only supported currencies
  • convertAmount is exported as a named utility alongside fetchRates
  • The function throws — callers handle the error in context
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.

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.

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 order

This makes three sequential network requests. For parallel requests, use Promise.all (next lesson).

  1. Implement the full api.js module as shown above.
  2. Call fetchRates() from main.js inside an async init() function with graceful fallback.
  3. Log the supported currencies and their current rates.
  4. Add an event listener on a <select> element that calls fetchRates(selectedCurrency) and logs the result. (No UI yet — just verify the async event handler works.)
  • async functions 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.
  • await in a for...of loop runs iterations sequentially. Use Promise.all for parallel.
  • Module-level let rates = { USD: 1 } provides a safe default while the real rates load.