M Logo
๐Ÿ“ TypeScript from Zero ยท Part 28 of 50

TypeScript: Save and Load a JSON File with node:fs/promises (readFile, writeFile)

November 6, 2026

Welcome to episode 28 of TypeScript from Zero, a series for complete beginners (no JavaScript needed). Welcome back to TypeScript from zero! A task tracker that forgets everything when it stops is not much use. Today the tasks go into a file. We continue in the tasks project, with a new file called store.ts. Every code block below is the exact code and output from the video, ready to copy.

Setup

We start in the terminal and open the project in VS Code:

bash
cd tasks
touch src/store.ts
code .

1. Save

We import read file and write file from node colon fs slash promises. This version of the file system tools returns promises, so we use them with await. Our file is called tasks.json. Save tasks turns the list into text with JSON stringify, and writes it. The two and the null make the file pretty, with two spaces of indentation. The return type is a promise of void.

2. Load

Load tasks does the opposite. It reads the file as text, in the encoding utf8, and turns the text back into a list with JSON parse.

3. First run

A small demo file. It loads the tasks and prints them. But the file does not exist yet, and Node stops with a real error: E NOENT, no such file or directory. That is the first run of every program that reads a file.

src/demo.ts:

ts
import { loadTasks, saveTasks } from "./store.ts";
console.log(await loadTasks());

src/store.ts:

ts
import { readFile, writeFile } from "node:fs/promises";
import type { Task } from "./task.ts";

const FILE = "tasks.json";

export async function saveTasks(tasks: Task[]): Promise<void> {
  await writeFile(FILE, JSON.stringify(tasks, null, 2));
}

export async function loadTasks(): Promise<Task[]> {
  const text = await readFile(FILE, "utf8");
  return JSON.parse(text);
}
text
$ node src/demo.ts
node:internal/modules/run_main:111
    triggerUncaughtException(
    ^

[Error: ENOENT: no such file or directory, open 'tasks.json'] {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: 'tasks.json'
}

Node.js v26.13.0

4. Fix

A missing file should simply mean: no tasks yet. We import exists sync from node colon fs, and at the start of load tasks we return an empty list if the file is not there.

src/store.ts:

ts
import { readFile, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import type { Task } from "./task.ts";

const FILE = "tasks.json";

export async function saveTasks(tasks: Task[]): Promise<void> {
  await writeFile(FILE, JSON.stringify(tasks, null, 2));
}

export async function loadTasks(): Promise<Task[]> {
  if (!existsSync(FILE)) {
    return [];
  }
  const text = await readFile(FILE, "utf8");
  return JSON.parse(text);
}
text
$ node src/demo.ts
[]

5. Save and load

Now the real thing. We save one task, and load again. The list is not empty any more. And we can look at the file: it is plain text, and we can read it, and even edit it by hand.

src/demo.ts:

ts
import { loadTasks, saveTasks } from "./store.ts";
console.log(await loadTasks());
await saveTasks([{ id: 1, title: "Buy milk", status: "todo" }]);
console.log(await loadTasks());
text
$ node src/demo.ts
[]
[ { id: 1, title: 'Buy milk', status: 'todo' } ]
text
$ cat tasks.json
[
  {
    "id": 1,
    "title": "Buy milk",
    "status": "todo"
  }
]

6. Error

A classic mistake: we forget the await. Now tasks is a promise, and the length is undefined. Node prints undefined, and moves on. tsc says: property length does not exist on type promise of task list, and it asks: did you forget to use await? TS2339.

src/demo.ts:

ts
import { loadTasks, saveTasks } from "./store.ts";
console.log(await loadTasks());
await saveTasks([{ id: 1, title: "Buy milk", status: "todo" }]);
console.log(await loadTasks());
const tasks = loadTasks();
console.log(tasks.length);
text
$ node src/demo.ts
[ { id: 1, title: 'Buy milk', status: 'todo' } ]
[ { id: 1, title: 'Buy milk', status: 'todo' } ]
undefined
text
$ npx tsc --noEmit
src/demo.ts:6:19 - error TS2339: Property 'length' does not exist on type 'Promise<Task[]>'.

6 console.log(tasks.length);
                    ~~~~~~

  src/demo.ts:6:19 - Did you forget to use 'await'?
    6 console.log(tasks.length);
                        ~~~~~~

Found 1 error in src/demo.ts:6

7. Fix

We delete those two lines, and tsc is quiet again.

src/demo.ts:

ts
import { loadTasks, saveTasks } from "./store.ts";
console.log(await loadTasks());
await saveTasks([{ id: 1, title: "Buy milk", status: "todo" }]);
console.log(await loadTasks());
bash
$ npx tsc --noEmit

Recap

Today: write file and read file with promises, JSON stringify and parse, a missing file on the first run, and the forgotten await. But there is a weak spot: JSON parse can return anything. Next episode: how to deal with that safely.

ยฉ 2026 All Rights Reserved By Mahmudul Alam