Reading and writing files
To keep notes after a restart, they must be written to disk. The main process reads and writes files with the node:fs/promises module from Node.js. What is left is to decide where the file goes: Electron has app.getPath for that.
The app data folder
app.getPath('userData') returns the folder the system gives the app for its data:
| System | Path |
|---|---|
| Windows | C:\Users\Anna\AppData\Roaming\notes |
| macOS | ~/Library/Application Support/notes |
| Linux | ~/.config/notes |
The last part of the path is the name field from package.json (or productName if set). The folder is created automatically.
The file path
Add to the top of main.js:
// main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('node:path');
const fs = require('node:fs/promises');
function notesFile() {
return path.join(app.getPath('userData'), 'notes.json');
}getPath works only after the app is ready, so the path is computed by a function at call time, not when the file is first run.
Reading
// main.js
async function loadNotes() {
try {
const json = await fs.readFile(notesFile(), 'utf8');
return JSON.parse(json);
} catch (error) {
if (error.code === 'ENOENT') {
return [];
}
throw error;
}
}On first launch the file does not exist yet — readFile throws an error with code ENOENT, and the function returns an empty list. Other errors are rethrown.
Writing
// main.js
async function saveNotes(notes) {
await fs.writeFile(notesFile(), JSON.stringify(notes, null, 2), 'utf8');
}JSON.stringify with an indent of 2 makes the file easy for a person to read.
IPC handlers
Connect the functions to the channels from the IPC lesson:
// main.js
app.whenReady().then(() => {
ipcMain.handle('notes:load', () => loadNotes());
ipcMain.handle('notes:save', (event, notes) => saveNotes(notes));
createWindow();
});// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('notes', {
load: () => ipcRenderer.invoke('notes:load'),
save: (notes) => ipcRenderer.invoke('notes:save', notes),
});A handler may return a promise — invoke on the page waits for it.
On the page
// renderer.js
let notes = [];
form.addEventListener('submit', async (event) => {
event.preventDefault();
notes.push(text.value);
await window.notes.save(notes);
text.value = '';
render();
});
window.notes.load().then((saved) => {
notes = saved;
render();
});The render function prints the notes array into the list, like showNotes in the previous lesson. Add a couple of notes and restart the app — they are still there. You can open notes.json in the app data folder.
Common mistake
Do not save data next to the code with path.join(__dirname, 'notes.json'). It works during development, but in a built app the code lives inside the app.asar archive or in Program Files, where writing is not allowed. Always keep user data in app.getPath('userData').