diff --git a/src/BizHawk.Client.Common/lua/ILuaLibraries.cs b/src/BizHawk.Client.Common/lua/ILuaLibraries.cs
index c4e1ea1078..dd66772f02 100644
--- a/src/BizHawk.Client.Common/lua/ILuaLibraries.cs
+++ b/src/BizHawk.Client.Common/lua/ILuaLibraries.cs
@@ -51,7 +51,7 @@ namespace BizHawk.Client.Common
 
 		void SpawnAndSetFileThread(string pathToLoad, LuaFile lf);
 
-		void ExecuteString(string command);
+		object[] ExecuteString(string command);
 
 		(bool WaitForFrame, bool Terminated) ResumeScript(LuaFile lf);
 	}
diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs
index 5ed96ca666..8cda0fbaa5 100644
--- a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs
+++ b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs
@@ -1388,18 +1388,14 @@ namespace BizHawk.Client.EmuHawk
 
 					LuaSandbox.Sandbox(null, () =>
 					{
-						LuaImp.ExecuteString($"console.log({rawCommand})");
-					}, () =>
-					{
-						LuaSandbox.Sandbox(null, () =>
+						var results = LuaImp.ExecuteString(rawCommand);
+						// empty array if the command was e.g. a variable assignment or a loop without return statement
+						// "void" functions return a single null
+						// if output didn't change, Print will take care of writing out "(no return)"
+						if (results is not [ ] and not [ null ] || OutputBox.Text == consoleBeforeCall)
 						{
-							LuaImp.ExecuteString(rawCommand);
-
-							if (OutputBox.Text == consoleBeforeCall)
-							{
-								WriteLine("Command successfully executed");
-							}
-						});
+							LuaLibraries.Print(results);
+						}
 					});
 
 					_messageCount = 0;
diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaLibraries.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaLibraries.cs
index 0490799b8f..002d04d9ca 100644
--- a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaLibraries.cs
+++ b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaLibraries.cs
@@ -323,8 +323,35 @@ namespace BizHawk.Client.EmuHawk
 		public void SpawnAndSetFileThread(string pathToLoad, LuaFile lf)
 			=> lf.Thread = SpawnCoroutine(pathToLoad);
 
-		public void ExecuteString(string command)
-			=> _lua.DoString(command);
+		/// <summary>
+		/// Executes Lua code. Automatically prepends <see langword="return"/> statement if possible.
+		/// </summary>
+		/// <returns>
+		/// Values returned by the Lua script, if any.
+		/// </returns>
+		public object[] ExecuteString(string command)
+		{
+			const string ChunkName = "input"; // shows up in error messages
+
+			// Use LoadString to separate parsing and execution, to tell syntax errors and runtime errors apart
+			LuaFunction func;
+			try
+			{
+				// Adding a return is necessary to get out return values of functions and turn expressions ("1+1" etc.) into valid statements
+				func = _lua.LoadString($"return {command}", ChunkName);
+			}
+			catch (Exception)
+			{
+				// command may be a valid statement without the added "return"
+				// if previous attempt couldn't be parsed, run the raw command
+				return _lua.DoString(command, ChunkName);
+			}
+
+			using (func)
+			{
+				return func.Call();
+			}
+		}
 
 		public (bool WaitForFrame, bool Terminated) ResumeScript(LuaFile lf)
 		{