diff --git a/src/005-passages/managePassages.js b/src/005-passages/managePassages.js
index b1d1e2f7731581e4b97d0ca64133c3d2704a5af6..e05ee918eb52a2180ce1368dbad4626d38f79458 100644
--- a/src/005-passages/managePassages.js
+++ b/src/005-passages/managePassages.js
@@ -1 +1,11 @@
+new App.DomPassage("Main",
+	() => {
+		V.nextButton = "END WEEK";
+		V.nextLink = "End Week";
+		V.encyclopedia = "How to Play";
+
+		return App.MainView.full();
+	}, ["jump-to-safe", "jump-from-safe"]
+);
+
 new App.DomPassage("Future Society", () => { return App.UI.fsPassage(); }, ["jump-to-safe", "jump-from-safe"]);
diff --git a/src/js/main.js b/src/js/main.js
index c5c79eaa41ef4ae343827bab732036b628f9773b..750d32686277c38d6865c7cac47a8c4311cb13cf 100644
--- a/src/js/main.js
+++ b/src/js/main.js
@@ -1,97 +1,3 @@
-/**
- * @returns {DocumentFragment}
- */
-App.MainView.errors = function() {
-	const fragment = document.createDocumentFragment();
-
-	/**
-	 * @returns {HTMLParagraphElement}
-	 */
-	function newError() {
-		const error = document.createElement("p");
-		fragment.append(error);
-		return error;
-	}
-
-	// check for correct version
-	if (V.releaseID >= 1000 || ["0.9", "0.8", "0.7", "0.6"].includes(V.ver)) {
-		if (V.releaseID < App.Version.release) {
-			newError().append(App.UI.DOM.makeElement("span", "INCOMPATIBILITY WARNING:", "major-warning"),
-				` Your saved game was created using version: ${V.ver}, build: ${V.releaseID}. You must run `,
-				App.UI.DOM.passageLink("Backwards Compatibility", "Backwards Compatibility"));
-		}
-	} else {
-		newError().append(App.UI.DOM.makeElement("span", "INCOMPATIBLE SAVE WARNING:", "major-warning"),
-			` Your saved game was created using version: ${V.ver}, and you are using a later version which New Game Plus cannot reconcile. Please start a new game.`);
-	}
-
-	// check for correct rules
-	if (V.defaultRules.length > 0 && (V.defaultRules[0].condition === undefined || V.defaultRules[0].set === undefined)) {
-		const error = newError();
-		error.append(App.UI.DOM.makeElement("span", "INCOMPATIBILITY WARNING:", "major-warning"),
-			" The rules assistant format has changed. In the Options Menu, please ");
-		const ra = document.createElement("strong");
-		ra.append("Reset RA Rules");
-		error.append(ra);
-	}
-
-	// check for NaN
-	if (V.NaNArray.length > 0) {
-		const error = newError();
-		error.id = "NaNArray";
-		error.append(App.UI.DOM.makeElement("span", "ERROR: The following variables are NaN! Please report this.", "error"));
-		V.NaNArray.forEach(e => {
-			const div = document.createElement("div");
-			div.append(e);
-			error.append(div);
-		});
-		error.append(App.UI.DOM.link("Hide NaN variables until next week",
-			() => {
-				error.outerHTML = "";
-				V.NaNArray = [];
-			})
-		);
-	}
-
-	// check custom slaves
-	if (App.Utils.IsCustomSlaveMutated(V.customSlave)) {
-		newError().append(App.UI.DOM.makeElement("span", "ERROR: Your custom slave order has taken on a mutated life of its own and has been summarily shot. Refile your custom slave order, if necessary, and notify the appropriate authorities if you see this message again.", "error"));
-		V.customSlave = new App.Entity.CustomSlaveOrder();
-	}
-	if (App.Utils.IsCustomSlaveMutated(V.huskSlave)) {
-		newError().append(App.UI.DOM.makeElement("span", "ERROR: Your husk slave order has taken on a mutated life of its own and has been summarily shot. Refile your husk slave order, if necessary, and notify the appropriate authorities if you see this message again.", "error"));
-		V.huskSlave = new App.Entity.CustomSlaveOrder();
-	}
-
-	return fragment;
-};
-
-/**
- * @returns {Text}
- */
-App.MainView.fcnn = function() {
-	let text;
-
-	if (V.FCNNstation !== 1 && V.week >= 90) {
-		text = "FCNN: FCNN service has been temporarily suspended. Please stand by.";
-	} else {
-		text = V.fcnn.random();
-	}
-
-	return document.createTextNode(`${text} `);
-};
-
-App.MainView.useFucktoys = function() {
-	const fragment = document.createDocumentFragment();
-	for (const slave of V.slaves) {
-		if (slave.assignment !== Job.FUCKTOY) {
-			continue;
-		}
-		fragment.append(App.MainView.useFucktoy(slave));
-	}
-	return fragment;
-};
-
 /**
  * @param {App.Entity.SlaveState} slave
  * @returns {HTMLDivElement}
@@ -210,18 +116,109 @@ App.MainView.useGuard = function() {
 	return outerDiv;
 };
 
-App.MainView.walkPast = function() {
-	const outerDiv = document.createElement("div");
+App.MainView.full = function() {
+	/**
+	 * @returns {DocumentFragment}
+	 */
+	function errors() {
+		const fragment = document.createDocumentFragment();
+
+		/**
+		 * @returns {HTMLParagraphElement}
+		 */
+		function newError() {
+			const error = document.createElement("p");
+			fragment.append(error);
+			return error;
+		}
+
+		// check for correct version
+		if (V.releaseID >= 1000 || ["0.9", "0.8", "0.7", "0.6"].includes(V.ver)) {
+			if (V.releaseID < App.Version.release) {
+				newError().append(App.UI.DOM.makeElement("span", "INCOMPATIBILITY WARNING:", "major-warning"),
+					` Your saved game was created using version: ${V.ver}, build: ${V.releaseID}. You must run `,
+					App.UI.DOM.passageLink("Backwards Compatibility", "Backwards Compatibility"));
+			}
+		} else {
+			newError().append(App.UI.DOM.makeElement("span", "INCOMPATIBLE SAVE WARNING:", "major-warning"),
+				` Your saved game was created using version: ${V.ver}, and you are using a later version which New Game Plus cannot reconcile. Please start a new game.`);
+		}
+
+		// check for correct rules
+		if (V.defaultRules.length > 0 && (V.defaultRules[0].condition === undefined || V.defaultRules[0].set === undefined)) {
+			const error = newError();
+			error.append(App.UI.DOM.makeElement("span", "INCOMPATIBILITY WARNING:", "major-warning"),
+				" The rules assistant format has changed. In the Options Menu, please ");
+			const ra = document.createElement("strong");
+			ra.append("Reset RA Rules");
+			error.append(ra);
+		}
 
-	const slave = V.slaves.filter(s => ![Job.BODYGUARD, Job.FUCKTOY].includes(s.assignment)).random();
-	if (slave) {
-		App.UI.DOM.appendNewElement("span", outerDiv, walkPast(slave), "scene-intro");
+		// check for NaN
+		if (V.NaNArray.length > 0) {
+			const error = newError();
+			error.id = "NaNArray";
+			error.append(App.UI.DOM.makeElement("span", "ERROR: The following variables are NaN! Please report this.", "error"));
+			V.NaNArray.forEach(e => {
+				const div = document.createElement("div");
+				div.append(e);
+				error.append(div);
+			});
+			error.append(App.UI.DOM.link("Hide NaN variables until next week",
+				() => {
+					error.outerHTML = "";
+					V.NaNArray = [];
+				})
+			);
+		}
+
+		// check custom slaves
+		if (App.Utils.IsCustomSlaveMutated(V.customSlave)) {
+			newError().append(App.UI.DOM.makeElement("span", "ERROR: Your custom slave order has taken on a mutated life of its own and has been summarily shot. Refile your custom slave order, if necessary, and notify the appropriate authorities if you see this message again.", "error"));
+			V.customSlave = new App.Entity.CustomSlaveOrder();
+		}
+		if (App.Utils.IsCustomSlaveMutated(V.huskSlave)) {
+			newError().append(App.UI.DOM.makeElement("span", "ERROR: Your husk slave order has taken on a mutated life of its own and has been summarily shot. Refile your husk slave order, if necessary, and notify the appropriate authorities if you see this message again.", "error"));
+			V.huskSlave = new App.Entity.CustomSlaveOrder();
+		}
+
+		return fragment;
 	}
 
-	return outerDiv;
-};
+	/**
+	 * All main passage logic that should be performed at the beginning ogf the passage but generate no visual output.
+	 */
+	function cleanup() {
+		if (V.tabChoice.SlaveInteract !== "Description") {
+			V.tabChoice.SlaveInteract = "Description";
+		}
 
-App.MainView.full = function() {
+		penthouseCensus();
+		V.costs = Math.trunc(calculateCosts.predict());
+		V.currentRule = V.defaultRules[0];
+		SlaveSort.slaves(V.slaves);
+
+		App.UI.SlaveList.ScrollPosition.restore();
+	}
+
+	/**
+	 * @returns {Text}
+	 */
+	function fcnn() {
+		let text;
+
+		if (V.FCNNstation !== 1 && V.week >= 90) {
+			text = "FCNN: FCNN service has been temporarily suspended. Please stand by.";
+		} else {
+			text = V.fcnn.random();
+		}
+
+		return document.createTextNode(`${text} `);
+	}
+
+	/**
+	 * @returns {HTMLDivElement}
+	 */
 	function mainMenu() {
 		const div = document.createElement("div");
 
@@ -269,30 +266,101 @@ App.MainView.full = function() {
 		return div;
 	}
 
-	const fragment = document.createDocumentFragment();
+	/**
+	 * @returns {DocumentFragment}
+	 */
+	function useFucktoys() {
+		const fragment = document.createDocumentFragment();
+		for (const slave of V.slaves) {
+			if (slave.assignment !== Job.FUCKTOY) {
+				continue;
+			}
+			fragment.append(App.MainView.useFucktoy(slave));
+		}
+		return fragment;
+	}
 
-	fragment.append(App.Reminders.list({maxFuture: 5, link: true}));
+	/**
+	 * @returns {HTMLDivElement}
+	 */
+	function walkPast() {
+		const outerDiv = document.createElement("div");
 
-	if (V.seeFCNN === 1) {
-		const div = document.createElement("div");
-		div.classList.add("main-fcnn");
-		div.append(App.MainView.fcnn(),
-			App.UI.DOM.passageLink("Hide", passage(), () => { V.seeFCNN = 0; })
-		);
-		fragment.append(div);
+		const slave = V.slaves.filter(s => ![Job.BODYGUARD, Job.FUCKTOY].includes(s.assignment)).random();
+		if (slave) {
+			App.UI.DOM.appendNewElement("span", outerDiv, globalThis.walkPast(slave), "scene-intro");
+		}
+
+		return outerDiv;
 	}
 
-	fragment.append(mainMenu());
+	/**
+	 * @returns {DocumentFragment}
+	 */
+	function assemble() {
+		const fragment = document.createDocumentFragment();
+		if (V.newModelUI === 1) {
+			fragment.append(V.building.render());
+		}
+
+		if (V.seeArcology === 1) {
+			fragment.append(App.Desc.playerArcology(
+				App.UI.DOM.passageLink("Hide", "Main", () => {V.seeArcology = 0;})));
+		}
+
+		if (V.seeDesk === 1) {
+			fragment.append(App.Desc.officeDescription(
+				App.UI.DOM.passageLink("Hide", "Main", () => {V.seeDesk = 0;})));
+		}
+
+		fragment.append(App.Reminders.list({maxFuture: 5, link: true}));
+
+		if (V.seeFCNN === 1) {
+			const div = document.createElement("div");
+			div.classList.add("main-fcnn");
+			div.append(fcnn(), App.UI.DOM.passageLink("Hide", passage(), () => { V.seeFCNN = 0; })
+			);
+			fragment.append(div);
+		}
+
+		fragment.append(mainMenu());
 
-	fragment.append(App.UI.SlaveList.penthousePage());
+		fragment.append(App.UI.SlaveList.penthousePage());
 
-	if (V.fucktoyInteractionsPosition === 0) {
-		fragment.append(App.MainView.useFucktoys());
+		if (V.fucktoyInteractionsPosition === 0) {
+			fragment.append(useFucktoys());
+		}
+		if (V.useSlaveSummaryOverviewTab === 0) {
+			fragment.append(App.MainView.useGuard());
+		}
+		fragment.append(walkPast());
+
+		return fragment;
 	}
-	if (V.useSlaveSummaryOverviewTab === 0) {
-		fragment.append(App.MainView.useGuard());
+
+	const fragment = document.createDocumentFragment();
+
+	fragment.append(errors());
+
+	// wrap everything in a try/catch statement, so App.MainView.errors() always gets shown. At the end print error out
+	// as well.
+	try {
+		cleanup();
+
+		fragment.append(assemble());
+	} catch (ex) {
+		App.UI.DOM.appendNewElement("p", fragment, `${ex.name}: ${ex.message}`, ["bold", "error"]);
+
+		const p = document.createElement("p");
+		const lines = ex.stack.split("\n");
+		for (const ll of lines) {
+			const div = document.createElement("div");
+			// remove file path from error message
+			div.append(ll.replace(/file:.*\//, "<path>/"));
+			p.append(div);
+		}
+		fragment.append(p);
 	}
-	fragment.append(App.MainView.walkPast());
 
 	return fragment;
 };
diff --git a/src/uncategorized/main.tw b/src/uncategorized/main.tw
deleted file mode 100644
index cd73e8b5f033317e518a90f032a661b8e827dcad..0000000000000000000000000000000000000000
--- a/src/uncategorized/main.tw
+++ /dev/null
@@ -1,33 +0,0 @@
-:: Main [nobr jump-to-safe jump-from-safe]
-
-<<includeDOM App.MainView.errors()>>
-
-<<set $nextButton = "END WEEK", $nextLink = "End Week", $encyclopedia = "How to Play">>
-
-<<if $tabChoice.SlaveInteract != 'Description'>>
-	<<set $tabChoice.SlaveInteract = 'Description'>>
-<</if>>
-
-<<run penthouseCensus()>>
-
-<<set $costs = Math.trunc(calculateCosts.predict())>>
-
-<<set $currentRule = $defaultRules[0]>>
-
-<<run SlaveSort.slaves($slaves)>>
-
-<<if $newModelUI == 1>>
-	<<includeDOM V.building.render()>>
-<</if>>
-
-<<if $seeArcology == 1>>
-	<<includeDOM App.Desc.playerArcology(App.UI.DOM.passageLink("Hide", "Main", () => {V.seeArcology = 0}))>>
-<</if>>
-
-<<if $seeDesk == 1>>
-	<<includeDOM App.Desc.officeDescription(App.UI.DOM.passageLink("Hide", "Main", () => {V.seeDesk = 0}))>>
-<</if>>
-
-<<includeDOM App.MainView.full()>>
-
-<<run App.UI.SlaveList.ScrollPosition.restore()>>