Local data and native APIs

Read and write the notes file

Create the data directory, handle a missing file, validate stored JSON, and write notes asynchronously.

Now we write the module that touches the disk. Create src/notes-store.js.

Notice that this file imports nothing from Electron. That’s deliberate. It receives a file path and a validator as arguments, so we can run it in plain Node.js and test it later without opening a window.

const fs = require('node:fs/promises')
const path = require('node:path')

async function readNotes(filePath, validateNotes) {
  try {
    const contents = await fs.readFile(filePath, 'utf8')
    return validateNotes(JSON.parse(contents))
  } catch (error) {
    if (error.code === 'ENOENT') return []
    throw error
  }
}

async function writeNotes(filePath, notes) {
  await fs.mkdir(path.dirname(filePath), { recursive: true })
  const temporaryPath = `${filePath}.tmp`

  await fs.writeFile(
    temporaryPath,
    JSON.stringify(notes, null, 2),
    'utf8'
  )
  await fs.rename(temporaryPath, filePath)
}

module.exports = { readNotes, writeNotes }

Let’s go through the two functions.

Reading

readNotes() reads the file, parses the JSON, and passes the result through validateNotes(), the same function we wrote for IPC input. Parsing and validating are separate steps. Valid JSON can still contain the wrong shape, like a note without a title, so we check both.

The catch block handles one specific case. ENOENT is the Node.js error code for “no such file or directory”. A missing file is normal: it means the app has no notes yet, so we return an empty list.

Every other error is different. Truncated JSON, a permission problem, a disk error. In those cases we rethrow. Never silently replace a broken file with [], because that erases the user’s notes. Surface the error and let the user decide.

Writing

writeNotes() first creates the folder with mkdir and recursive: true. On a fresh install the subdirectory doesn’t exist yet, and the call does nothing when it does.

Then comes the part I want you to pay attention to. We don’t write to notes.json directly. We write to notes.json.tmp and then rename it over the real file.

Why? If the app crashes, or the computer loses power, in the middle of a write, we’d be left with half a JSON document. With the temporary file, the visible notes.json is replaced only after the complete new file exists on disk. The rename is a single operation.

We use node:fs/promises throughout, so the main process stays responsive while the operating system works. A synchronous write would freeze every window for the duration.

What this does not do

This is not a backup system. Disk errors, edits made by other programs, and filesystem quirks can still bite. When a write fails, keep the original error, keep the unsaved state in the renderer, and for data that matters consider versioned backups.

Test the module by hand with three files: no file at all, a file with valid notes, and a file with truncated JSON (delete the last few characters). Only the first case should come back as an empty list. The third must throw.

Lesson completed