All exercises use the tea shop data. Start by loading it:
import { teas } from "../../data/teas.js";Functions in JavaScript are "first-class citizens" - they can be assigned to variables, stored in arrays, and passed around like any other value.
📚 Recall: First-Class Functions
A first-class function can be: assigned to a variable, passed as an argument, returned from a function, stored in data structures. JavaScript treats functions like any other value.
Create a function that logs a tea's name and origin in the format "Sencha (Japan)". Assign it to a variable called logTea. Call it with the first tea in the array.
const logTea = function (tea) {
// your code
};
logTea(teas[0]); // should log: "Sencha (Japan)"Create a function called functionRunner that takes a function as a parameter and calls it.
function functionRunner(fn) {
// call the function that was passed in
}
// Test it:
functionRunner(function () {
console.log("I was called!");
});
// Also test with a function variable:
const sayHello = function () {
console.log("Hello!");
};
functionRunner(sayHello);💡 This is the core of callbacks: passing a function to another function that calls it.
Create an array containing three different functions. Each function should log something different. Loop through the array and call each function.
const functions = [
function () {
console.log("First");
},
// add two more
];
for (let i = 0; i < functions.length; i++) {
functions[i](); // call each function
}Create a function createGreeter(greeting) that returns a new function. The returned function should take a name and log the greeting with the name.
const sayHello = createGreeter("Hello");
const sayHi = createGreeter("Hi");
sayHello("Alice"); // "Hello, Alice!"
sayHi("Bob"); // "Hi, Bob!"💡 This pattern is called a "function factory" - a function that creates and returns other functions.
reduce builds up a single value from an array. It's the most powerful array method.
📚 Recall: Accumulator
The accumulator is the value being "built up" through each iteration. It starts as your initial value (the second argument to reduce) and ends as your final result.
Use reduce to calculate the total stockCount across all teas.
const totalStock = teas.reduce((sum, tea) => {
// return the new sum
}, 0);
console.log(totalStock); // sum of all stockCount valuesCalculate the total inventory value: the sum of pricePerGram * stockCount for each tea.
const inventoryValue = teas.reduce(/* ... */);
console.log(inventoryValue);Use reduce to count how many teas of each type exist.
const countByType = teas.reduce((counts, tea) => {
// increment counts[tea.type]
// hint: counts[tea.type] might be undefined the first time
}, {});
console.log(countByType);
// Expected: { green: 6, black: 6, herbal: 4, oolong: 2, white: 2 }Use reduce to group tea names by their origin country.
const groupedByOrigin = teas.reduce(/* ... */);
console.log(groupedByOrigin);
// Expected: { Japan: ["Sencha", "Matcha", ...], China: [...], ... }💡 This solves Exercise 4 from Week 1's assignment! Now you know the proper way to implement it.
Let's build our own versions of array methods to understand how callbacks work under the hood.
📚 Recall: Callback
A callback is a function you pass to another function, to be called at some point. In teas.map(tea => tea.name), the arrow function IS the callback - map calls it for each item.
Create your own myForEach(array, callback) function that works like the built-in forEach.
function myForEach(array, callback) {
// loop through array
// call callback for each item
}
// Test it:
myForEach(teas, function (tea) {
console.log(tea.name);
});Create your own myMap(array, callback) function that works like the built-in map.
function myMap(array, callback) {
const result = [];
// loop through array
// call callback for each item
// push the return value to result
return result;
}
// Test it:
const names = myMap(teas, function (tea) {
return tea.name;
});
console.log(names); // ["Sencha", "Earl Grey", ...]Create your own myFilter(array, callback) function that works like the built-in filter.
function myFilter(array, callback) {
// your implementation
}
// Test it:
const organic = myFilter(teas, function (tea) {
return tea.organic;
});
console.log(organic.length); // number of organic teas💡 Building these yourself demystifies array methods. They're just functions that call your callback at the right time.
Now the twist: what if the callback runs later? setTimeout is the simplest example - it waits, then calls your callback.
📚 Recall: Delayed Execution
With delayed execution, the code doesn't wait. "Start this operation, continue running other code, call the callback when done." Unlike regular code which runs line-by-line and blocks.
What order will these console.logs appear? Write your prediction first, then run the code to check.
console.log("1. Starting");
setTimeout(function () {
console.log("2. Timeout done");
}, 1000);
console.log("3. Continuing");Create a function runAfterDelay(delay, callback) that waits delay milliseconds, then calls the callback.
function runAfterDelay(delay, callback) {
// use setTimeout
}
// Test it:
runAfterDelay(2000, function () {
console.log("This runs after 2 seconds");
});
runAfterDelay(1000, function () {
console.log("This runs after 1 second");
});
console.log("This runs immediately");What order do the three messages appear?
Create a function findTeaById(id, callback) that simulates a database lookup with a 500ms delay.
function findTeaById(id, callback) {
// Use setTimeout to wait 500ms
// Inside the timeout: find the tea by id, then call the callback with it
}
// Test it:
console.log("Looking up tea...");
findTeaById(3, function (tea) {
console.log("Found:", tea.name);
});
console.log("Request sent, waiting...");Before running: predict the output order. Then run it to check.
Expected output order:
- "Looking up tea..."
- "Request sent, waiting..."
- (after 500ms) "Found: Dragon Well"
Call findTeaById three times in a row with different IDs. Notice that all three requests start at the same time - they don't wait for each other.
findTeaById(1, function (tea) {
console.log("Got:", tea.name);
});
findTeaById(5, function (tea) {
console.log("Got:", tea.name);
});
findTeaById(10, function (tea) {
console.log("Got:", tea.name);
});
console.log("All requests sent!");💡 "All requests sent!" appears first, then all three results appear together after 500ms. This is how backends handle multiple requests efficiently.
Node.js file operations use callbacks. fs.readFile takes a callback that runs when the file is loaded.
📚 Recall: Error-First Callback
Node.js convention: callbacks receive (error, result). If error exists, something went wrong - handle it and return early. If error is null/undefined, proceed with result.
First, create a file called orders.json in the same folder with this content:
[
{ "id": 1, "customerId": 101, "items": [{ "teaId": 1, "grams": 100 }] },
{ "id": 2, "customerId": 102, "items": [{ "teaId": 3, "grams": 50 }, { "teaId": 8, "grams": 30 }] },
{ "id": 3, "customerId": 103, "items": [{ "teaId": 5, "grams": 200 }] }
]Then write code that reads the file and logs how many orders there are:
import fs from "fs";
fs.readFile("./orders.json", { encoding: "utf8" }, function (error, data) {
if (error) {
console.error(error);
return;
}
// 1. Parse the JSON string into an array
// 2. Log the number of orders
});Try to point to a non-existent file to see how the error handling works.
Expected output: Number of orders: 3
Building on Exercise 16, after reading the orders:
- For each order, look up the tea prices from the teas array
- Calculate the total value of each order (
pricePerGram * grams) - Log each order's total
// Expected output:
// Order 1: 12.00 DKK (1 item)
// Order 2: 26.00 DKK (2 items)
// Order 3: 36.00 DKK (1 item)💡 You'll need to find each tea by ID, then multiply pricePerGram by grams.