This is a well-documented "feature" of TRY...CATCH. From https://msdn.microsoft.com/en-us/library/ms175976.aspx:
The following types of errors are not handled by a CATCH block when they occur at the same level of execution as the TRY…CATCH construct:
Compile errors, such as syntax errors, that prevent a batch from running.
Errors that occur during statement-level recompilation, such as object name resolution errors that occur after compilation because of deferred name resolution.
These errors are returned to the level that ran the batch, stored procedure, or trigger.
(emphasis mine)
They actually then give an example of exactly this sort of thing, with a nonexistent table causing an error that bypasses the CATCH block.
To test this, you could cause the error to occur at a level of execution lower than the TRY...CATCH construct, either by putting the UPDATEs in a stored procedure and calling the procedure from the TRY...CATCH construct, or as in my quick example based on yours, by running it using sp_executesql:
DECLARE @RunDate DATETIME = DATEADD(hh,2,CAST(CAST(DATEADD(day,1,GETUTCDATE()) as DATE) as DATETIME))
DECLARE @sql nvarchar(max)='BEGIN TRANSACTION
UPDATE
[TableA]
SET
ScheduleDate = @RunDateInner
UPDATE
[Table B]
SET
ScheduleDate = @RunDateInner
COMMIT TRANSACTION'
BEGIN TRY
EXECUTE sp_executesql @statement=@sql,
@params=N'@RundateInner datetime',
@RunDateInner=@Rundate
END TRY
BEGIN CATCH
If @@TRANCOUNT >0
ROLLBACK tran
END CATCH
I hope this helps!