This chapter covers
You’ve built your application, and you’re ready to distribute it to the world. You’ve prepared your announcement, and your cursor is hovering over the Publish button when—all of a sudden—your mind begins to race with some very important questions: What if it doesn’t work on all platforms as you expected? What if there is a bug you haven’t encountered yet? What if you need to push out a new version of the application to your users?
These concerns are all valid. The good news is that Electron has you covered on each front. In this chapter, we cover how to collect crash reports and—in the event you receive an unexpectedly large number of them—how to push an update to all of the users that currently have the application installed on their system.
Despite how good your skills as a developer are, your application is going to have bugs. Some users report them, but many others suffer in silence. Even among the users who report bugs, it can be difficult to determine how common a given issue is among your user base as a whole. Crash reporting is critical for discovering what types of problems occur and at what frequency your users are experiencing them.
Though it is certainly a relief that Electron includes support for crash reporting, it’s surprising. As we’ve covered throughout this book, Electron is built on top of Chromium, and crash reporting is certainly something that the Chromium team deals with in their project. To stand on the shoulder of giants, however, we need to set up the crash reporter in our Electron application, as well as deploy a simple server to collect these crash reports for later analysis.
In this chapter, we implement crash reporting and auto-updating into Fire Sale, which is the application we began getting ready for deployment in chapter 14. We begin working on the chapter-15-beginning branch, found at https://github.com/electron-in-action/firesale.
We tackle the following:
Under the hood, Electron uses one of two crash-reporting systems: on macOS, the Chrome team’s Crashpad reporting engine; on Windows and Linux, an older engine known as Breakpad. These options impact how we implement crash reporting in Electron. The main difference is that Breakpad crash reporting needs to be set up only in the main process, whereas Crashpad must be started in the main and renderer processes.
Yes, you could copy and paste the code from the main process into each of the renderer processes. (We’re lucky to have only one in Fire Sale.) But that means you’d have to remember to be diligent enough to change it in multiple places in the event you needed to update the configuration. Personally, I’d prefer to spend a few extra moments of effort now for a lifetime of laziness. Let’s set up a single function that we can use in both places.
const { crashReporter } = require('electron');
const host = 'http://localhost:3000/'; 1
const config = {
productName: 'Fire Sale',
companyName: 'Electron in Action',
submitURL: host + 'crashreports',
uploadToServer: true, 2
};
crashReporter.start(config); 3
console.log('[INFO] Crash reporting started.', crashReporter);
module.exports = crashReporter;
We set up a reusable module for starting up the crash reporter, but you can change the value of any of those options. Particularly, we need to change submitURL because it is both unlikely that our users happen to be running a server on that port and ultimately unhelpful to us if they’re collecting crash reports locally and never sending them to us.
The code by itself does not start the crash-reporting engine unless it is pulled into the main and renderer processes. We start by pulling it into the main process.
const { app, BrowserWindow, dialog, Menu } = require('electron');
const createApplicationMenu = require('./application-menu');
const fs = require('fs');
require('./crash-reporter'); 1
const windows = new Set();
const openFiles = new Map();
// The rest of the main process code...
This method is effectively enough to implement the ability to send crash reports on Windows and Linux. We can verify that the code inside the crash reporter has executed by checking the terminal for the message we logged to the console in listing 15.1 when the application starts, as shown in figure 15.1.

As I mentioned earlier, we need to do one more—arguably simple—step to get crash reporting working on macOS: execute the same code in the renderer process.
const { remote, ipcRenderer, shell } = require('electron');
const { Menu } = remote;
const path = require('path');
const mainProcess = remote.require('./main.js');
const currentWindow = remote.getCurrentWindow();
require('./crash-reporter'); 1
We can verify that the crash reporter has started by opening the Developer Tools in the Fire Sale UI and confirming that the console message has been logged correctly, as seen in figure 15.2.

We can send crash reports—but where? We can trigger a crash by typing process.crash() into the Developer Tools in the renderer process. As shown in figures 15.3 and 15.4, doing so results in the following: the page crashing, the Developer Tools locking up, and the main process logging an error.


The immediate issue that we need to solve is the error in our terminal. The exact contents of the error varies by operating system, but the gist is that the crash reporter cannot connect to the server. This makes sense, because we haven’t yet set up a server.
Depending on your application, you may already be using an API server. If so, I’d recommend adding an endpoint for receiving POST requests from your application. That said, Fire Sale has not had the need for an API server, and I’m going to work under the assumption that we don’t have one. For those of you that do, I suspect what follows is still helpful because it illustrates what kind of payload you can expect when a crash is reported.
As described in listing 15.4, let’s create a simple server for capturing and recording crash reports. The code can be found on GitHub at http://mng.bz/k6eT.
const express = require('express');
const multer = require('multer');
const bodyParser = require('body-parser');
const uuid = require('uuid');
const writeFile = require('write-file');
const path = require('path');
const http = require('http');
const app = express(); 1
const server = http.createServer(app);
app.use(bodyParser.urlencoded({ extended: false })); 2
const crashesPath = path.join(__dirname, 'crashes');
const upload = multer({ 3
dest: crashesPath,
}).single('upload_file_minidump');
app.post('/crashreports', upload, (request, response) => {
const body = { 4
...request.body,
filename: request.file.filename,
date: new Date(),
};
const filePath = `${request.file.path}.json`;
const report = JSON.stringify(body);
writeFile(filePath, report, error => { 5
if (error) return console.error('Error Saving', report);
console.log('Crash Saved', filePath, report);
});
response.end();
});
server.listen(3000, () => {
console.log('Crash report server running on Port 3000.');
});
If you clone the library from GitHub, install the dependencies, and run npm start. You should have a server ready and able to collect crash reports running on http://localhost:3000, which is exactly where we pointed the Electron crash reporter in listing 15.1.
Each crash report creates two files in the ./crashes directory: a mini-dump from Chromium and a JSON file with additional metadata about the crash. The JSON file contains useful data about the version of your application that crashed and the platform that it was running on.
{
"_companyName": "Electron in Action",
"_productName": "Fire Sale",
"_version": "1.0.0",
"guid": "46cecb8f-f2de-4159-a235-dc8713f8393f",
"platform": "darwin", 1
"process_type": "renderer", 2
"prod": "Electron",
"ver": "2.0.4", 3
"filename": "a51d2ca2e13cad25cea6c2bd15ae4d4d" 4
}
When a crash is reported, the server saves two files to the /crashes directory: a51d2ca2e13cad25cea6c2bd15ae4d4d and a51d2ca2e13cad25cea6c2bd15ae4d4d.json. The former is a dump from Chromium of everything that was happening when the application crashed. This file is in a binary format and cannot be opened in a text editor.
The easiest way to parse this file is to use the minidump library from the Electron team. You can install this globally using npm install -g minidump. After you install it, you have the minidump_stackwalk command-line tool at your disposal and can use minidump_stackwalk <name of mini-dump file> to read the contents of the dump. Alternatively, you can use this tool programmatically on your crash-report server as outlined in the tool’s documentation (www.npmjs.com/package/minidump).
Electron’s built-in crash reporter is good at what it claims to do on the box—reporting crashes. But in my experience, general bugs and minor errors are much more common than outright crashes. It would be great if we could collect these errors as well, to have better insight into where the rough edges of our application are and exactly how to fix them.
Unfortunately, this is not a tool that is already built into Electron. The good news is that this feature is relatively easy to implement given the knowledge we’ve already gained throughout the course of this book.
To implement this feature, we have to listen for any uncaught exceptions. The syntax for this is slightly different between the main and renderer processes, but the implementation is the same, as shown in the following listing. When an uncaught exception occurs, send an HTTP POST request to our crash server, which will then log the error to a JSON file.
const { crashReporter } = require('electron');
const request = require('request');
const manifest = require('../package.json');
const host = 'http://localhost:3000/';
const config = {
productName: 'Fire Sale',
companyName: 'Electron in Action',
submitURL: host + 'crashreports',
uploadToServer: true,
};
crashReporter.start(config);
const sendUncaughtException = error => { 1
const { productName, companyName } = config;
request.post(host + 'uncaughtexceptions', { 2
form: {
_productName: productName,
_companyName: companyName,
_version: manifest.version,
platform: process.platform,
process_type: process.type,
ver: process.versions.electron,
error: { 3
name: error.name,
message: error.message,
stack: error.stack,
},
},
});
};
if (process.type === 'browser') { 4
process.on('uncaughtException', sendUncaughtException); 5
} else {
window.addEventListener('error', sendUncaughtException); 6
}
console.log('[INFO] Crash reporting started.', crashReporter);
module.exports = crashReporter;
Any time an error is fired and it bubbles up to the window object without being handled, a report is sent to our server. But just as last time, our server has not been set up yet to handle this report. We need to add another route to log this error.
const express = require('express');
const multer = require('multer');
const bodyParser = require('body-parser');
const uuid = require('uuid');
const writeFile = require('write-file');
const path = require('path');
const http = require('http');
const app = express();
const server = http.createServer(app);
app.use(bodyParser.urlencoded({ extended: false }));
const crashesPath = path.join(__dirname, 'crashes');
const exceptionsPath = path.join(__dirname, 'uncaughtexceptions'); 1
const upload = multer({
dest: crashesPath,
}).single('upload_file_minidump');
app.post('/crashreports', upload, (request, response) => {
// ...
});
app.post('/uncaughtexceptions', (request, response) => {
const filePath = path.join(exceptionsPath, `${uuid()}.json`); 2
const report = JSON.stringify({
...request.body,
date: new Date() 3
});
writeFile(filePath, report, error => { 4
if (error) return console.error('Error Saving', report);
console.log('Exception Saved', filePath, report);
});
response.end();
});
server.listen(3000, () => {
console.log('Crash report server running on Port 3000.');
});
In chapter 14, we packaged our application so that users don’t need to have Node installed or be familiar with the command line to start it up. In the previous section, we added crash and exception reporting to be confident that we’ll be notified in the event our application does not work as intended. Our next step is to sign our application so that our users can be certain that they’re getting the real thing and not a cheap substitute.
This process differs on macOS and Windows, so let’s walk through each platform separately. Although some of the steps for macOS seem repetitive when we package Fire Sale for the Mac App Store in chapter 16, myriad subtle differences exist.
Before we get into the nitty-gritty of code signing our applications, I’ll take a moment to explain what code signing is. The internet is a wild place. You can think of code signing as a tamper-proof seal around your application. Signing our applications allows users to be confident that it both came from you and that no one modified it or otherwise tampered with it along the way. On top of being a generally good idea, macOS and Windows prefer that users work with signed applications and present a series of warnings if they attempt to open an unsigned application. Depending on their settings, users may not be able to open unsigned applications at all.
You need a few prerequisites in place to sign a macOS application. First, you must be a registered member of the Apple Developer Program (https://developer.apple.com/programs/). Second, you must have Xcode installed. This can be downloaded from the Mac App Store or from Apple’s Developer site (https://developer.apple.com/xcode/download/). In addition to having Xcode installed, you also need to have the Xcode command-line tools installed. To set these up, type xcode-select –install from the command line, and follow all of the subsequent prompts.
To sign your application, you need to create certificates either through iTunes Connect or from within Xcode. We spend an uncomfortable amount of time in iTunes Connect in chapter 16, so we’ll do it in Xcode this time.
We generate two certificates: a “Developer ID Application” and a “Developer ID Installer” certificate. To create these certificates



Xcode automatically adds these certificates to your Keychain. Electron Packager looks these up when it comes time to package your application and sign it. First, we need to modify the packaging script in our package.json.
"build-mac": "electron-packager . --platform=darwin--out=build --icon=./icons/Icon.icns --asar –overwrite
--app-bundle-id=\"net.stevekinney.firesale\"
--app-version=\"1.0.0\" --build-version=\"1.0.100\" --osx-sign",
This line is getting a bit long, and we begin to break it out a bit in chapter 16, but for now, we add a few important new arguments to the existing script: --app-bundle-id, --app-version, --build-version, and --osx-sign.
When running npm run build-mac, you are prompted for your password multiple times, as shown in figure 15.8, because Electron Packager accesses your keychain to use the certificates to sign your application.

On Windows, you need a code-signing certificate. Microsoft recommends buying a certificate from a list of certificate authorities listed in their documentation (http://mng.bz/3VEH).
You can still build an installer without a certificate, but you might have difficulty distributing your application to users without one. Microsoft’s SmartScreen filter may block your application from being downloaded, and many antivirus programs might mislabel your application as malware. For the purposes of this chapter, as we go through the process of building an installer, I show you where to plug in your certificate, should you move forward and decide to purchase one.
The Electron team maintains a helpful library for building installers on Windows. You can install this tool using npm install --save-dev electron-winstaller. We create a new folder called scripts and make a file called scripts/windows.js to store the configuration for our Windows installer. Add the content in listing 15.9 to the file we just created.
const { createWindowsInstaller } = require('electron-winstaller'); 1
const path = require('path');
const iconPath = path.resolve(__dirname, '../icons/Icon.ico'); 2
const result = createWindowsInstaller({ 3
title: 'Fire Sale',
authors: 'Steve Kinney',
appDirectory: path.resolve( 4
__dirname,
'../build/Fire Sale-win32-x64'
),
outputDirectory: path.resolve( 5
__dirname,
'../build/Fire Sale-win32-x64-installer'
),
icon: iconPath, 6
setupIcon: iconPath, 7
name: 'FireSale',
setupExe: 'FireSaleSetup.exe',
setupMsi: 'FireSaleSetup.msi',
});
result
.then(() => console.log('Success')) 8
.catch(error => console.error('Failed', error)); 9
If you have your certificates handy, you can add them to the configuration object. certificateFile should point to the path where the certificate is located. This process is similar to how we locate the iconPath. certificatePassword is the password for the certificate. Do not store the password in version control—particularly if your application is or ever will be open source.
If you get really excited and double-click the FireSaleSetup executable, you may notice that things are a little strange. You see a loading GIF followed by the application immediately opening. It didn’t add a shortcut to the desktop, or any other way of getting back to the application.
Uninstalling the application adds its own set of oddities. When you click the button in the Add and Remove Programs settings panel, you see the application open again before it finally uninstalls. We need to address this.
The Windows installer is set up with the Squirrel.Windows framework. When you start the application for the first time, or when it is being uninstalled, has been updated, or finds an available update, Squirrel passes an argument to your application. Luckily, working with Squirrel on Windows can range from incredibly simple to very easy. Let’s start with the incredibly simple way, and then I’ll show you what’s happening under the hood.
The easiest way to get started is to delegate the work to someone else by installing electron-squirrel-startup using npm install electron-squirrel-startup. After you install this package, add the code from this listing to your ./app/main.js.
const { app, BrowserWindow, dialog, Menu } = require('electron');
const createApplicationMenu = require('./application-menu');
const fs = require('fs');
require('./crash-reporter');
if(require('electron-squirrel-startup')) return; 1
// ...
With that simple change, all the oddities we encountered when working with FireSaleSetup.exe are squared away. So, what’s happening in this module? The module is open source and can be found on GitHub (https://github.com/mongodb-js/electron-squirrel-startup), but it’s a small file, and for the sake of completeness I list it here as well.
var path = require('path');
var spawn = require('child_process').spawn;
var debug = require('debug')('electron-squirrel-startup');
var app = require('electron').app;
var run = function(args, done) {
var updateExe = path.resolve( 1
path.dirname(process.execPath), '..', 'Update.exe'
);
debug('Spawning `%s` with args `%s`', updateExe, args);
spawn(updateExe, args, {
detached: true
}).on('close', done);
};
var check = function() {
if (process.platform === 'win32') { 2
var cmd = process.argv[1]; 3
debug('processing squirrel command `%s`', cmd);
var target = path.basename(process.execPath);
if (cmd === '--squirrel-install' || cmd === '--squirrel-updated') { 4
run(['--createShortcut=' + target + ''], app.quit); 5
return true; 6
}
if (cmd === '--squirrel-uninstall') { 7
run(['--removeShortcut=' + target + ''], app.quit); 8
return true;
}
if (cmd === '--squirrel-obsolete') {
app.quit();
return true;
}
}
return false; 9
};
module.exports = check();
If Squirrel did anything, it returns true. Recall in ./app/main.js that we used a conditional and if require(’electron-squirrel-startup’) returned true, we ended the main process early. This allows us to tap into the Squirrel framework when necessary and not start the application when we’re installing or uninstalling it. You can also see that on installation and uninstallation, Squirrel creates and removes the desktop shortcut as needed.
Whether you’ve completed a hot new feature that you want everyone to have or you’re trying to correct a critical bug that you found after implementing the crash and error report, the ability to push out updates to your users is important.
We tend to take this capability for granted as web developers. Whenever we deploy a new application to the web, we can reasonably expect that users are getting the latest and greatest version of it. But that’s not necessarily true when we’re building desktop applications. We can tell users about it, but there is no guarantee that they’re going to take the time to download it.
Browsers used to suffer from this problem. New JavaScript language and web platform features would be released, but you couldn’t use them because users typically did not update their browsers on a regular basis. Today, modern browsers like Chrome and Firefox push out new versions to users every six weeks or so. The update is automatically downloaded, and the next time the user starts the browser, the new one is swapped in without them having to think about the tedious process of upgrading.
It shouldn’t be a surprise at this point to hear that Electron provides a mechanism for us to do this with our applications—with some exceptions. As of this writing, this feature is limited to macOS and Windows. There is currently no support for auto-updating Electron applications on Linux.
Like crash reporting, implementing automatic updates has two sides: your application and a server to host releases of your application. We get into the details momentarily, but let’s talk about how this works at a high level first. After you’ve configured the autoUpdater module, it pings your release server every time the application starts. If the server responds with an HTTP 204 response, then Electron knows that it is running the latest version of the application. If there is a new version, the server returns an HTTP 200 JSON-formatted response with the URL of the new release.
You may have your own server for hosting releases. That’s perfectly fine. If you do not, we create a simple server to get you off the ground and demonstrate auto-updating in action. But this is not meant to be prescriptive. As long as you have a server that responds with the correct HTTP status codes and payload if there is a new release, Electron does not have strong opinions on the implementation details, and we won’t either, for the time being.
To get this working, we check if we’re using a production version of the application. If so, we send a request to the server asking for the most recent version of the application. If a new version exists, ask the user if they are interested in updating. When they agree, tell the autoUpdater module to quit the application and install the new version on our behalf.
const { app, autoUpdater, dialog, BrowserWindow } = require('electron');
const isDevelopment = app.getPath('exe').indexOf('electron') !== -1; 1
const baseUrl = 'https://firesale-releases.glitch.me'; 2
const platform = process.platform; 3
const currentVersion = app.getVersion(); 4
const releaseFeed =
`${baseUrl}/releases/${platform}?currentVersion=${currentVersion}`; 5
if (isDevelopment) {
console.info('[AutoUpdater]', 'In Developement Mode. Skipping...'); 6
} else {
console.info('[AutoUpdater]', `Setting release feed to ${releaseFeed}.`);
autoUpdater.setFeedURL(releaseFeed); 7
}
autoUpdater.addListener('update-available', () => { 8
dialog.showMessageBox({
type: 'question',
buttons: ['Install & Relaunch', 'Not Now'],
defaultId: 0,
message: `${app.getName()} has been updated!`,
detail: 'An update has been downloaded and can be installed now.'
}, response => {
if (response === 0) {
setTimeout(() => {
app.removeAllListeners('window-all-closed'); 9
BrowserWindow.getAllWindows().forEach(win => win.close()); 10
autoUpdater.quitAndInstall(); 11
}, 0);
}
});
});
module.exports = autoUpdater;
On macOS, autoUpdater fails if the application has not been code signed. When we’re developing the application, we use a version that has not been signed. These two facts are in direct opposition. The best course of action is not to fetch an update if we’re in development. This outcome also makes sense, because we’re likely working on the next version of the application, as opposed to the most recently released version.
You can implement this in a few ways. You could, for example, try to set the feed URL and then catch the error if the application hasn’t been code signed. Based purely on aesthestic reasons, this is not something I like to do. Electron does not have an API to let us know whether we’re in development, but when we are in development, we’re using a version of Electron buried inside of our node_modules directory. In my case, it’s located at ./node_modules/electron/dist/Electron.app/Contents/MacOS/Electron. In production it is located in the application bundle itself. I’ve taken advantage of this fun fact to determine whether the application is in development.
If it’s not in development, then we create a URL to look for updates based on the platform on which the application is running. This URL can technically be anything you want. You don’t have to follow my example, if your setup is different than the one we write together next.
We tell the autoUpdater module to send a request to the URL provided and ask if there are any updates. If there are, then the updates-available event runs. We’ve set up a listener for that event. If an update is available, we ask the user if they would like to update to the latest and greatest version of Fire Sale. If they agree, then we take a series of steps to gracefully transition them to the new version.
First, I remove the event listener that we set up in ./app/main.js that prevents the application from quitting if all of the windows are closed on macOS. Next, we iterate through the windows and close them. We do this to prompt the user to save any changes made to the file. Finally, we call autoUpdater.quitAndInstall(), which—unsuprisingly—closes the application and installs the newly downloaded update. Squirrel takes care of all of this on our behalf.
As with the crash reporter, we set up a deliberately simple server for notifying your users of updates. A more robust example might use a database to store the most recent version along with release notes and more. You could store your application bundles on S3, but going through the process of creating a full-featured web server is outside the scope of this book.
As I mentioned earlier, our server must fulfill a simple contract to play nicely with Electron’s autoUpdater module. If the application matches the latest release, the server should return a response with a 204 status code. If an update is available, the servers should return a JSON object that has a url property with the URL to the new application as its value.
I’ve hosted the server (https://firesale-releases.glitch.me/) and its code (https://glitch.com/edit/#!/firesale-releases) on Glitch, where you can remix it for your own purposes. Glitch also hosts the application, so we can use it in Fire Sale to pull down an update. I encourage you to visit the link to the previous code for the most recent version, but I include an annotated version here as well.
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
app.use(express.static('public'));
const latestRelease = '1.2.0'; 1
app.get("/", (request, response) => {
response.sendFile(__dirname + '/views/index.html');
});
app.get('/releases/:platform', (request, response) => { 2
const { platform } = request.params; 3
const { currentVersion } = request.query; 4
if (currentVersion === latestRelease) { 5
response.status(204); 6
return response.end();
}
if (platform === 'darwin') { 7
return response.json({
url: ...
});
}
if (platform === 'win32') { 8
return response.json({
url: ...
});
}
if (platform === 'linux') { 9
return response.json({
url: ...
});
}
response.status(404).end();
});
const listener = app.listen(process.env.PORT, () => {
console.log('Your app is listening on port ' + listener.address().port);
});
I started by storing a reference to the most recent version of Fire Sale in a variable. Next, I set up a dynamic route. If you recall, Fire Sale requests updates based on the platform on which it’s running. Versions running on macOS, Windows, and Linux request updates from /releases/darwin, /releases/win32, and /releases/linux, respectively. We also check if the version number is included as a query parameter.
If the current version and the latest release match, then we respond with a 204 status code to let the application know that it is currently running the most recent version. If they do not match, we assume that an update is available. We then check which platform the user requested and send them the URL for the appropriate flavor of the latest release of Fire Sale, a shown in figure 15.9.

If you’ve used the code I provided for the autoUpdater and pointed it at https://firesale-releases.glitch.me/, then you should be notified about a very important update to Fire Sale.