Why isn't it caught? Because TRY / CATCH doesn't catch all types of errors. But you can get it to catch the non-system-critical stuff by wrapping it in an EXEC(), such as:
The error in the ALTER will fail out to the EXEC, which in turn will return to the TRY / CATCH block reporting a simple, catchable error.
For more detail, the MSDN page for TRY...CATCH states:
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.
That second bullet point, about object name resolution errors, is what is causing the ALTER statement to fail and not be eligible to get handled by the CATCH block.
Regarding your statement about "DDL statements are implicit COMMIT": I am not sure what you mean by this exactly, but all (well, nearly all) queries are transactions by themselves. Meaning, single-query batches have no use for explicit
BEGIN TRAN /
ROLLBACK. If the statement fails it automatically rolls back, and if it succeeds then it automatically commits. You would only use the
BEGIN TRAN /
ROLLBACK construct if you want to conditionally rollback for some reason. It is still a good idea to use the
TRY...CATCH structure to properly handle errors, but you gain nothing (at least with the code posted in the question) with the explicit transaction handling. There is absolutely no difference between the posted code and the following:
SELECT 'I''m in Catch';