What you'll learn
Files are a durable boundary between your program and the operating system. You will use promise-based APIs, build portable paths, serialize JSON, and handle real file failures without blocking the event loop.
By the end of this lesson, you'll be able to:
- Read and write UTF-8 files with node:fs/promises
- Build cross-platform paths with node:path
- Distinguish module-relative locations from the current working directory
- Persist and recover structured JSON data safely
Core mental model
Node.js becomes easier when you separate the JavaScript language from the runtime and the operating-system capabilities it exposes. Use this table as a decision guide.
| Concept | What it means | Decision rule |
|---|---|---|
| Async file API | A promise settles after the file operation completes | Prefer fs/promises in application paths that must remain responsive |
| Working directory | process.cwd() is where the process was launched | Use it for user-selected project-relative input |
| Module directory | The location of the source module itself | Use it for assets shipped beside the module |
| Serialization | Converts in-memory data to a stable text representation | Validate parsed JSON before trusting its shape |
Professional workflow
Build and verify Node.js programs from the terminal in small, observable steps.
- Decide whether a path belongs to the caller's working directory or to the module.
- Construct the path with path utilities or a file URL instead of manual separators.
- Perform the operation directly and handle its error; avoid a separate exists check race.
- Specify text encoding explicitly and validate parsed data.
- Write a complete replacement to a temporary sibling before renaming when durability matters.
- Test missing, malformed, empty, and permission-denied paths.
Keep the feedback loop short
Guided code lab
Read a module-relative text file
new URL resolves the asset beside this module no matter which folder launched the process.
import { readFile } from "node:fs/promises";
const templateUrl = new URL("./welcome.txt", import.meta.url);
try {
const message = await readFile(templateUrl, "utf8");
console.log(message.trim());
} catch (error) {
if (error.code === "ENOENT") {
console.error("welcome.txt is missing");
process.exitCode = 1;
} else {
throw error;
}
}Create a portable report path
path.join uses the correct separator for the current platform. recursive mkdir makes repeated setup safe.
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
const outputDirectory = join(process.cwd(), "reports");
const outputFile = join(outputDirectory, "summary.json");
const report = { generatedAt: new Date().toISOString(), lessons: 6 };
await mkdir(outputDirectory, { recursive: true });
await writeFile(outputFile, JSON.stringify(report, null, 2) + "
", "utf8");
console.log("Saved", outputFile);Load JSON with shape validation
JSON syntax can be valid while the data shape is wrong. Parse errors and validation errors need different messages.
import { readFile } from "node:fs/promises";
export async function loadTasks(fileUrl) {
try {
const text = await readFile(fileUrl, "utf8");
const value = JSON.parse(text);
if (!Array.isArray(value)) throw new TypeError("tasks must be an array");
return value;
} catch (error) {
if (error.code === "ENOENT") return [];
if (error instanceof SyntaxError) {
throw new Error("Tasks file contains invalid JSON", { cause: error });
}
throw error;
}
}Production practice
Use promises
Prefer fs/promises for clear async control flow; reserve synchronous APIs for controlled startup or one-off scripts where blocking is acceptable.
Validate data
Treat every file as external input. Validate parsed types, required fields, ranges, and identifiers.
Name the path base
Make working-directory-relative versus module-relative intent obvious in variable names and documentation.
Common failure mode
Independent workshop
Build a JSON-backed notes repository with list, add, and remove operations that works on Windows, macOS, and Linux.
Your finished workshop must include:
- Promise-based file operations
- Portable path construction
- Empty-state behavior for a missing file
- Clear errors for malformed JSON and invalid records
- Pretty-printed output ending with a newline
Definition of done
Recap & quick check
Key takeaways
- fs/promises keeps file workflows composable with async/await
- Path intent matters as much as path syntax
- path utilities avoid platform-specific separators
- JSON parsing requires shape validation
- Handle operation errors instead of pre-checking
Quick check
1. Relative fs paths are normally resolved against what?
2. Which API family returns promises for file operations?
3. Does valid JSON guarantee the expected application shape?
4. Why use path.join?
Next: Async Foundations, Events & Errors