The answer from @Tamas Tárnok led me down the right path, so I will mark he answer as the solution.

It seems that if you use `job.Status.Failed = true`, the exception message will actually be displayed in a popup. Let me demonstrate:

    public class ImportCommand : Command
    {
    	private readonly ICommandArgFactory _commandArgFactory;
    
    	public ImportCommand(
    		ICommandArgFactory commandArgFactory
    	{
    		if (commandArgFactory == null) throw new ArgumentNullException(nameof(commandArgFactory));
    		_commandArgFactory = commandArgFactory;
    	}
    
    	public override void Execute(CommandContext context)
    	{
    		NameValueCollection parameters = _commandArgFactory.GetArgs(context);
    
    		Sitecore.Context.ClientPage.Start(this, "Import", new ClientPipelineArgs(parameters));
    	}
    
    	private void Import(ClientPipelineArgs args)
    	{
    		if (args.IsPostBack)
    		{
    			if (args.Result == "no") return;
    
    			ProgressBox.Execute("Import", "Import", DoImport, args);
    		}
    		else
    		{
    			SheerResponse.Confirm("Are you sure?");
    			args.WaitForPostBack();
    		}
    	}
    
    	private void DoImport(object[] parameters)
    	{
    		BaseJob job = Sitecore.Context.Job;
    
    		if (job == null) return;
    
    		try
            {
                ClientPipelineArgs args = parameters[0] as ClientPipelineArgs;
    
                job.Status.State = JobState.Running;
                ...code here...
                job.Status.AddMessage("Parsing some more...");
                job.Status.State = JobState.Finished;
            }
            catch (CustomException customEx)
            {
                Log.Error(customEx.Message, this);
    			job.Status.LogError(customEx.Message);
    			job.Status.Failed = true;
            }
            catch (Exception ex)
            {
                Log.Error(ex.Message, this);
    			job.Status.LogError(ex.Message);
                job.Status.Failed = true;
            }
    	}
    }