diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/droidtooth/DroidtoothPlugin.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/droidtooth/DroidtoothPlugin.java
index 20ca21d11a41e63c152451805af1250867e6f4e1..9597fa512f1d99fe1b708fbcd5975e42bc1d4d7a 100644
--- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/droidtooth/DroidtoothPlugin.java
+++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/droidtooth/DroidtoothPlugin.java
@@ -18,6 +18,7 @@ import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
 import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
 import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
 import org.briarproject.bramble.api.plugin.Backoff;
+import org.briarproject.bramble.api.plugin.PluginException;
 import org.briarproject.bramble.api.plugin.TransportId;
 import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
 import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
@@ -128,7 +129,7 @@ class DroidtoothPlugin implements DuplexPlugin {
 	}
 
 	@Override
-	public boolean start() throws IOException {
+	public void start() throws PluginException {
 		if (used.getAndSet(true)) throw new IllegalStateException();
 		// BluetoothAdapter.getDefaultAdapter() must be called on a thread
 		// with a message queue, so submit it to the AndroidExecutor
@@ -142,13 +143,14 @@ class DroidtoothPlugin implements DuplexPlugin {
 					}).get();
 		} catch (InterruptedException e) {
 			Thread.currentThread().interrupt();
-			throw new IOException("Interrupted while getting BluetoothAdapter");
+			LOG.warning("Interrupted while getting BluetoothAdapter");
+			throw new PluginException(e);
 		} catch (ExecutionException e) {
-			throw new IOException(e);
+			throw new PluginException(e);
 		}
 		if (adapter == null) {
 			LOG.info("Bluetooth is not supported");
-			return false;
+			throw new PluginException();
 		}
 		running = true;
 		// Listen for changes to the Bluetooth state
@@ -170,7 +172,6 @@ class DroidtoothPlugin implements DuplexPlugin {
 				LOG.info("Not enabling Bluetooth");
 			}
 		}
-		return true;
 	}
 
 	private void bind() {
diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPlugin.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPlugin.java
index b3daa5f13605cc67e71ea35a7a8fcf6507497b9f..4d507d0d1304d853413d7b4c06475f5e6eb3abce 100644
--- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPlugin.java
+++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPlugin.java
@@ -39,14 +39,13 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
 	}
 
 	@Override
-	public boolean start() {
+	public void start() {
 		if (used.getAndSet(true)) throw new IllegalStateException();
 		running = true;
 		// Register to receive network status events
 		networkStateReceiver = new NetworkStateReceiver();
 		IntentFilter filter = new IntentFilter(CONNECTIVITY_ACTION);
 		appContext.registerReceiver(networkStateReceiver, filter);
-		return true;
 	}
 
 	@Override
diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java
index 4d9ddbcbcdff42cea281407698d38e8407970db0..f0cf90e7c3a86703b98a898831d0f90b026e659d 100644
--- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java
+++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java
@@ -25,6 +25,7 @@ import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
 import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
 import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
 import org.briarproject.bramble.api.plugin.Backoff;
+import org.briarproject.bramble.api.plugin.PluginException;
 import org.briarproject.bramble.api.plugin.TorConstants;
 import org.briarproject.bramble.api.plugin.TransportId;
 import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
@@ -162,14 +163,18 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 	}
 
 	@Override
-	public boolean start() throws IOException {
+	public void start() throws PluginException {
 		if (used.getAndSet(true)) throw new IllegalStateException();
 		// Install or update the assets if necessary
 		if (!assetsAreUpToDate()) installAssets();
 		LOG.info("Starting Tor");
 		// Watch for the auth cookie file being updated
-		cookieFile.getParentFile().mkdirs();
-		cookieFile.createNewFile();
+		try {
+			cookieFile.getParentFile().mkdirs();
+			cookieFile.createNewFile();
+		} catch (IOException e) {
+			throw new PluginException(e);
+		}
 		CountDownLatch latch = new CountDownLatch(1);
 		FileObserver obs = new WriteObserver(cookieFile, latch);
 		obs.startWatching();
@@ -182,8 +187,8 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 		Process torProcess;
 		try {
 			torProcess = Runtime.getRuntime().exec(cmd, env, torDirectory);
-		} catch (SecurityException e) {
-			throw new IOException(e);
+		} catch (SecurityException | IOException e) {
+			throw new PluginException(e);
 		}
 		// Log the process's standard output until it detaches
 		if (LOG.isLoggable(INFO)) {
@@ -197,35 +202,39 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 			if (exit != 0) {
 				if (LOG.isLoggable(WARNING))
 					LOG.warning("Tor exited with value " + exit);
-				return false;
+				throw new PluginException();
 			}
 			// Wait for the auth cookie file to be created/updated
 			if (!latch.await(COOKIE_TIMEOUT, MILLISECONDS)) {
 				LOG.warning("Auth cookie not created");
 				if (LOG.isLoggable(INFO)) listFiles(torDirectory);
-				return false;
+				throw new PluginException();
 			}
 		} catch (InterruptedException e) {
 			LOG.warning("Interrupted while starting Tor");
 			Thread.currentThread().interrupt();
-			return false;
+			throw new PluginException();
 		}
-		// Open a control connection and authenticate using the cookie file
-		controlSocket = new Socket("127.0.0.1", CONTROL_PORT);
-		controlConnection = new TorControlConnection(controlSocket);
-		controlConnection.authenticate(read(cookieFile));
-		// Tell Tor to exit when the control connection is closed
-		controlConnection.takeOwnership();
-		controlConnection.resetConf(Collections.singletonList(OWNER));
-		running = true;
-		// Register to receive events from the Tor process
-		controlConnection.setEventHandler(this);
-		controlConnection.setEvents(Arrays.asList(EVENTS));
-		// Check whether Tor has already bootstrapped
-		String phase = controlConnection.getInfo("status/bootstrap-phase");
-		if (phase != null && phase.contains("PROGRESS=100")) {
-			LOG.info("Tor has already bootstrapped");
-			connectionStatus.setBootstrapped();
+		try {
+			// Open a control connection and authenticate using the cookie file
+			controlSocket = new Socket("127.0.0.1", CONTROL_PORT);
+			controlConnection = new TorControlConnection(controlSocket);
+			controlConnection.authenticate(read(cookieFile));
+			// Tell Tor to exit when the control connection is closed
+			controlConnection.takeOwnership();
+			controlConnection.resetConf(Collections.singletonList(OWNER));
+			running = true;
+			// Register to receive events from the Tor process
+			controlConnection.setEventHandler(this);
+			controlConnection.setEvents(Arrays.asList(EVENTS));
+			// Check whether Tor has already bootstrapped
+			String phase = controlConnection.getInfo("status/bootstrap-phase");
+			if (phase != null && phase.contains("PROGRESS=100")) {
+				LOG.info("Tor has already bootstrapped");
+				connectionStatus.setBootstrapped();
+			}
+		} catch (IOException e) {
+			throw new PluginException(e);
 		}
 		// Register to receive network status events
 		networkStateReceiver = new NetworkStateReceiver();
@@ -233,7 +242,6 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 		appContext.registerReceiver(networkStateReceiver, filter);
 		// Bind a server socket to receive incoming hidden service connections
 		bind();
-		return true;
 	}
 
 	private boolean assetsAreUpToDate() {
@@ -246,7 +254,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 		}
 	}
 
-	private void installAssets() throws IOException {
+	private void installAssets() throws PluginException {
 		InputStream in = null;
 		OutputStream out = null;
 		try {
@@ -269,7 +277,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 		} catch (IOException e) {
 			tryToClose(in);
 			tryToClose(out);
-			throw e;
+			throw new PluginException(e);
 		}
 	}
 
@@ -476,7 +484,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 	}
 
 	@Override
-	public void stop() throws IOException {
+	public void stop() throws PluginException {
 		running = false;
 		tryToClose(socket);
 		if (networkStateReceiver != null)
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/Plugin.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/Plugin.java
index 89cd78640a1c8d0ef1a2b69656bc8abd3f405c6b..f37ef3f07ed82aeb23b49eaa78b75ea4db19b227 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/Plugin.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/Plugin.java
@@ -3,7 +3,6 @@ package org.briarproject.bramble.api.plugin;
 import org.briarproject.bramble.api.contact.ContactId;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 
-import java.io.IOException;
 import java.util.Collection;
 
 @NotNullByDefault
@@ -25,14 +24,14 @@ public interface Plugin {
 	int getMaxIdleTime();
 
 	/**
-	 * Starts the plugin and returns true if it started successfully.
+	 * Starts the plugin.
 	 */
-	boolean start() throws IOException;
+	void start() throws PluginException;
 
 	/**
 	 * Stops the plugin.
 	 */
-	void stop() throws IOException;
+	void stop() throws PluginException;
 
 	/**
 	 * Returns true if the plugin is running.
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/PluginException.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/PluginException.java
new file mode 100644
index 0000000000000000000000000000000000000000..ab66575a4047d859f8d48f7314877ebecc567336
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/PluginException.java
@@ -0,0 +1,15 @@
+package org.briarproject.bramble.api.plugin;
+
+/**
+ * An exception that indicates an error starting or stopping a {@link Plugin}.
+ */
+public class PluginException extends Exception {
+
+	public PluginException() {
+		super();
+	}
+
+	public PluginException(Throwable cause) {
+		super(cause);
+	}
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/PluginManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/PluginManagerImpl.java
index 45eeb2d44c956b54769bb09daec6164a2cea6b19..442f3ed89245a3c868d858d489e66118293af154 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/PluginManagerImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/PluginManagerImpl.java
@@ -11,6 +11,7 @@ import org.briarproject.bramble.api.plugin.ConnectionManager;
 import org.briarproject.bramble.api.plugin.Plugin;
 import org.briarproject.bramble.api.plugin.PluginCallback;
 import org.briarproject.bramble.api.plugin.PluginConfig;
+import org.briarproject.bramble.api.plugin.PluginException;
 import org.briarproject.bramble.api.plugin.PluginManager;
 import org.briarproject.bramble.api.plugin.TransportConnectionReader;
 import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
@@ -30,7 +31,6 @@ import org.briarproject.bramble.api.settings.Settings;
 import org.briarproject.bramble.api.settings.SettingsManager;
 import org.briarproject.bramble.api.ui.UiCallback;
 
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -193,24 +193,17 @@ class PluginManagerImpl implements PluginManager, Service {
 		@Override
 		public void run() {
 			try {
-				try {
-					long start = System.currentTimeMillis();
-					boolean started = plugin.start();
-					long duration = System.currentTimeMillis() - start;
-					if (started) {
-						if (LOG.isLoggable(INFO)) {
-							LOG.info("Starting plugin " + plugin.getId()
-									+ " took " + duration + " ms");
-						}
-					} else {
-						if (LOG.isLoggable(WARNING)) {
-							LOG.warning("Plugin" + plugin.getId()
-									+ " did not start");
-						}
-					}
-				} catch (IOException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
+				long start = System.currentTimeMillis();
+				plugin.start();
+				long duration = System.currentTimeMillis() - start;
+				if (LOG.isLoggable(INFO)) {
+					LOG.info("Starting plugin " + plugin.getId() + " took " +
+							duration + " ms");
+				}
+			} catch (PluginException e) {
+				if (LOG.isLoggable(WARNING)) {
+					LOG.warning("Plugin " + plugin.getId() + " did not start");
+					LOG.log(WARNING, e.toString(), e);
 				}
 			} finally {
 				startLatch.countDown();
@@ -246,10 +239,13 @@ class PluginManagerImpl implements PluginManager, Service {
 							+ " took " + duration + " ms");
 				}
 			} catch (InterruptedException e) {
-				LOG.warning("Interrupted while waiting for plugin to start");
+				LOG.warning("Interrupted while waiting for plugin to stop");
 				// This task runs on an executor, so don't reset the interrupt
-			} catch (IOException e) {
-				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			} catch (PluginException e) {
+				if (LOG.isLoggable(WARNING)) {
+					LOG.warning("Plugin " + plugin.getId() + " did not stop");
+					LOG.log(WARNING, e.toString(), e);
+				}
 			} finally {
 				stopLatch.countDown();
 			}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java
index 1708e918fec6ef8f9b0841dc7169b05983944fd4..a1bed00752770e10c74ad08c73ddd723c20ae1dd 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java
@@ -101,11 +101,10 @@ abstract class TcpPlugin implements DuplexPlugin {
 	}
 
 	@Override
-	public boolean start() {
+	public void start() {
 		if (used.getAndSet(true)) throw new IllegalStateException();
 		running = true;
 		bind();
-		return true;
 	}
 
 	protected void bind() {
diff --git a/bramble-j2se/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java b/bramble-j2se/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java
index 091aa9425b09e96315427c481ff35f3ad6406afa..92407910399e9a73d5f2e16a6e31a1e94d11d253 100644
--- a/bramble-j2se/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java
+++ b/bramble-j2se/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java
@@ -9,6 +9,7 @@ import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
 import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
 import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
 import org.briarproject.bramble.api.plugin.Backoff;
+import org.briarproject.bramble.api.plugin.PluginException;
 import org.briarproject.bramble.api.plugin.TransportId;
 import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
 import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
@@ -99,7 +100,7 @@ class BluetoothPlugin implements DuplexPlugin {
 	}
 
 	@Override
-	public boolean start() throws IOException {
+	public void start() throws PluginException {
 		if (used.getAndSet(true)) throw new IllegalStateException();
 		// Initialise the Bluetooth stack
 		try {
@@ -108,13 +109,14 @@ class BluetoothPlugin implements DuplexPlugin {
 			// On Linux the user may need to install libbluetooth-dev
 			if (OsUtils.isLinux())
 				callback.showMessage("BLUETOOTH_INSTALL_LIBS");
-			return false;
+			throw new PluginException(e);
+		} catch (BluetoothStateException e) {
+			throw new PluginException(e);
 		}
 		if (LOG.isLoggable(INFO))
 			LOG.info("Local address " + localDevice.getBluetoothAddress());
 		running = true;
 		bind();
-		return true;
 	}
 
 	private void bind() {
diff --git a/bramble-j2se/src/main/java/org/briarproject/bramble/plugin/file/RemovableDrivePlugin.java b/bramble-j2se/src/main/java/org/briarproject/bramble/plugin/file/RemovableDrivePlugin.java
index 5a5e99a2bfbede62063aaf03cee7e1f3d98ca682..f2b687b53d0e43a07c429b7c932e30c47811f8a1 100644
--- a/bramble-j2se/src/main/java/org/briarproject/bramble/plugin/file/RemovableDrivePlugin.java
+++ b/bramble-j2se/src/main/java/org/briarproject/bramble/plugin/file/RemovableDrivePlugin.java
@@ -2,6 +2,7 @@ package org.briarproject.bramble.plugin.file;
 
 import org.briarproject.bramble.api.contact.ContactId;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.PluginException;
 import org.briarproject.bramble.api.plugin.TransportId;
 import org.briarproject.bramble.api.plugin.simplex.SimplexPluginCallback;
 
@@ -42,17 +43,24 @@ class RemovableDrivePlugin extends FilePlugin
 	}
 
 	@Override
-	public boolean start() throws IOException {
+	public void start() throws PluginException {
 		if (used.getAndSet(true)) throw new IllegalStateException();
 		running = true;
-		monitor.start(this);
-		return true;
+		try {
+			monitor.start(this);
+		} catch (IOException e) {
+			throw new PluginException(e);
+		}
 	}
 
 	@Override
-	public void stop() throws IOException {
+	public void stop() throws PluginException {
 		running = false;
-		monitor.stop();
+		try {
+			monitor.stop();
+		} catch (IOException e) {
+			throw new PluginException(e);
+		}
 	}
 
 	@Override
diff --git a/bramble-j2se/src/main/java/org/briarproject/bramble/plugin/modem/ModemPlugin.java b/bramble-j2se/src/main/java/org/briarproject/bramble/plugin/modem/ModemPlugin.java
index 5169e5d5dc61b8348a9db27aa04f5a6d29117723..98094f36d0811e1b63930b7fcf1a8a1acfd29a85 100644
--- a/bramble-j2se/src/main/java/org/briarproject/bramble/plugin/modem/ModemPlugin.java
+++ b/bramble-j2se/src/main/java/org/briarproject/bramble/plugin/modem/ModemPlugin.java
@@ -6,6 +6,7 @@ import org.briarproject.bramble.api.data.BdfList;
 import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
 import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
 import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
+import org.briarproject.bramble.api.plugin.PluginException;
 import org.briarproject.bramble.api.plugin.TransportId;
 import org.briarproject.bramble.api.plugin.duplex.AbstractDuplexTransportConnection;
 import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
@@ -68,7 +69,7 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback {
 	}
 
 	@Override
-	public boolean start() {
+	public void start() throws PluginException {
 		if (used.getAndSet(true)) throw new IllegalStateException();
 		for (String portName : serialPortList.getPortNames()) {
 			if (LOG.isLoggable(INFO))
@@ -79,12 +80,12 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback {
 				if (LOG.isLoggable(INFO))
 					LOG.info("Initialised modem on " + portName);
 				running = true;
-				return true;
+				return;
 			} catch (IOException e) {
 				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 			}
 		}
-		return false;
+		throw new PluginException();
 	}
 
 	@Override
diff --git a/bramble-j2se/src/test/java/org/briarproject/bramble/plugin/modem/ModemPluginTest.java b/bramble-j2se/src/test/java/org/briarproject/bramble/plugin/modem/ModemPluginTest.java
index c516acb175a5d9ba3056b4515fc9c24ec2b1b614..77db390d33985a2c109dda169c11d1c6449a80da 100644
--- a/bramble-j2se/src/test/java/org/briarproject/bramble/plugin/modem/ModemPluginTest.java
+++ b/bramble-j2se/src/test/java/org/briarproject/bramble/plugin/modem/ModemPluginTest.java
@@ -14,7 +14,6 @@ import java.util.Map;
 
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
 
 public class ModemPluginTest extends BrambleTestCase {
 
@@ -49,7 +48,7 @@ public class ModemPluginTest extends BrambleTestCase {
 			oneOf(modem).start();
 			will(returnValue(true));
 		}});
-		assertTrue(plugin.start());
+		plugin.start();
 		context.assertIsSatisfied();
 	}
 
@@ -88,7 +87,7 @@ public class ModemPluginTest extends BrambleTestCase {
 			oneOf(modem).dial(NUMBER);
 			will(returnValue(true));
 		}});
-		assertTrue(plugin.start());
+		plugin.start();
 		// A connection should be returned
 		assertNotNull(plugin.createConnection(contactId));
 		context.assertIsSatisfied();
@@ -129,7 +128,7 @@ public class ModemPluginTest extends BrambleTestCase {
 			oneOf(modem).dial(NUMBER);
 			will(returnValue(false));
 		}});
-		assertTrue(plugin.start());
+		plugin.start();
 		// No connection should be returned
 		assertNull(plugin.createConnection(contactId));
 		context.assertIsSatisfied();
@@ -177,7 +176,7 @@ public class ModemPluginTest extends BrambleTestCase {
 			oneOf(modem).start();
 			will(returnValue(true));
 		}});
-		assertTrue(plugin.start());
+		plugin.start();
 		// No connection should be returned
 		assertNull(plugin.createConnection(contactId));
 		context.assertIsSatisfied();