Repository: bytecodealliance/wasm-tools
Pull request #1333 (2023-12-12 22:38:07)

Update the exception-handling validator implementation
This commit updates the implementation of the exception-handling proposal to the latest version of the proposal. This was already implemented in the text format but all other crates needed updating. The changes were:

* A new `exn` heap type is added for `exnref`. Currently this isn't a subtype with anything else, and this probably isn't correct but it's at least conservative for now.

* The `try`, `delegate`, `catch`, `catch_all`, and `rethrow` instructions were all removed as they're no longer present.

* New `try_table` and `throw_ref` instructions were added.

* Support for the name section subsection for tags has been added along with printing tag names in the text format.

* All exception-handling spec tests are now re-enabled.

osa1: > I expect that Firefox folks might want wasmprinter and wast to continue to support the old exception instructions for the indefinite future

FWIW, V8 main branch currently supports both old and new instructions.

I don't know how long they plan to support both versions.
alexcrichton: AFAIK no one's using the historical version of the validation in wasmparser. Wasmtime for example has never had an implementation of the exception handling proposal. In that sense while it would make sense to keep the text parsing if Firefox is using it I'm not sure it's necessary to keep anything else if there aren't users.
eqrion: > AFAIK no one's using the historical version of the validation in wasmparser. Wasmtime for example has never had an implementation of the exception handling proposal. In that sense while it would make sense to keep the text parsing if Firefox is using it I'm not sure it's necessary to keep anything else if there aren't users.

Firefox will need the text parsing of legacy instructions around for the forseeable future. We don't use wasmparser for anything.

I do personally use wasm-tools to disassemble binaries from the wild, so would like if at-least the binary to text pipeline continues to work.
eqrion: > > I expect that Firefox folks might want wasmprinter and wast to continue to support the old exception instructions for the indefinite future
> 
> FWIW, V8 main branch currently supports both old and new instructions.
> 
> I don't know how long they plan to support both versions.

The browser vendors will keep supporting both versions until usage of the old drops to the point of being able to be deprecated. That's a fuzzy line though, and could take many years or never be achievable.
alexcrichton: Ok I'll look to preserve the text parsing, although I'll perhaps remove some of the more sugared forms such as `(try (do ..` since removing that simplified things greatly. (unless y'all need that in Firefox?). I'll remove all validation from `wasmparser` since no one needs it (I think) and it shouldn't be too bad to keep the instructions around for awhile and print them as well.

I'll get back to this in the new year.
eqrion: > Ok I'll look to preserve the text parsing, although I'll perhaps remove some of the more sugared forms such as `(try (do ..` since removing that simplified things greatly. (unless y'all need that in Firefox?). I'll remove all validation from `wasmparser` since no one needs it (I think) and it shouldn't be too bad to keep the instructions around for awhile and print them as well.
> 
> I'll get back to this in the new year.

I don't think we need the sugared forms. If we do use them in tests, I can rewrite them to avoid it. Thanks!
eqrion: ```rust
impl<'a> Parse<'a> for TryTable<'a> {
    fn parse(parser: Parser<'a>) -> Result<Self> {
        let block = parser.parse()?;
        let mut catches = Vec::new();
        while parser.peek2::<kw::catch>()?
            || parser.peek2::<kw::catch_ref>()?
            || parser.peek2::<kw::catch_all>()?
            || parser.peek2::<kw::catch_all_ref>()?
        {
            catches.push(parser.parens(|p| {
```

This one's my fault, unrelated to your PR here, the above code is incorrect for the following test case:
```
try_table
  try
  catch_all
  end
end
```

peek2 in try_table will see catch_all and think that it should be parsing a parentheses.

Would you be able to fix that as well, while you're here?
alexcrichton: > I'll get back to this in the new year.

Ok I've added a commit now which means that the PR now should only remove the s-expression-based form of the old opcodes (e.g. `(try (do ...`) as well as validation. Otherwise parsing the text format, parsing the binary format, and printing to the text format should all continue to work.

> Would you be able to fix that as well, while you're here?

Sure!

I suspect it'd still fall over on `try_table (catch_all)` which technically is valid if both the old-and-the-new are supported, but unless you need support for that I think I'll probably just leave that in.
alexcrichton: Oh oops here I was waiting for an approval I already got, I'll queue this up for merge now

Patch:
diff --git a/crates/wasm-encoder/src/core/code.rs b/crates/wasm-encoder/src/core/code.rs
index 851a807c..888f14b5 100644
--- a/crates/wasm-encoder/src/core/code.rs
+++ b/crates/wasm-encoder/src/core/code.rs
@@ -310,10 +310,6 @@ pub enum Instruction<'a> {
     Loop(BlockType),
     If(BlockType),
     Else,
-    Try(BlockType),
-    Delegate(u32),
-    Catch(u32),
-    CatchAll,
     End,
     Br(u32),
     BrIf(u32),
@@ -333,7 +329,15 @@ pub enum Instruction<'a> {
         ty: u32,
         table: u32,
     },
+    TryTable(BlockType, Cow<'a, [Catch]>),
     Throw(u32),
+    ThrowRef,
+
+    // Deprecated exception-handling instructions
+    Try(BlockType),
+    Delegate(u32),
+    Catch(u32),
+    CatchAll,
     Rethrow(u32),
 
     // Parametric instructions.
@@ -1013,6 +1017,9 @@ impl Encode for Instruction<'_> {
                 sink.push(0x09);
                 l.encode(sink);
             }
+            Instruction::ThrowRef => {
+                sink.push(0x0A);
+            }
             Instruction::End => sink.push(0x0B),
             Instruction::Br(l) => {
                 sink.push(0x0C);
@@ -1079,6 +1086,12 @@ impl Encode for Instruction<'_> {
                 [ty].encode(sink);
             }
 
+            Instruction::TryTable(ty, ref catches) => {
+                sink.push(0x1f);
+                ty.encode(sink);
+                catches.encode(sink);
+            }
+
             // Variable instructions.
             Instruction::LocalGet(l) => {
                 sink.push(0x20);
@@ -3114,6 +3127,40 @@ impl Encode for Instruction<'_> {
     }
 }
 
+#[derive(Clone, Debug)]
+#[allow(missing_docs)]
+pub enum Catch {
+    One { tag: u32, label: u32 },
+    OneRef { tag: u32, label: u32 },
+    All { label: u32 },
+    AllRef { label: u32 },
+}
+
+impl Encode for Catch {
+    fn encode(&self, sink: &mut Vec<u8>) {
+        match self {
+            Catch::One { tag, label } => {
+                sink.push(0x00);
+                tag.encode(sink);
+                label.encode(sink);
+            }
+            Catch::OneRef { tag, label } => {
+                sink.push(0x01);
+                tag.encode(sink);
+                label.encode(sink);
+            }
+            Catch::All { label } => {
+                sink.push(0x02);
+                label.encode(sink);
+            }
+            Catch::AllRef { label } => {
+                sink.push(0x03);
+                label.encode(sink);
+            }
+        }
+    }
+}
+
 /// A constant expression.
 ///
 /// Usable in contexts such as offsets or initializers.
diff --git a/crates/wasm-encoder/src/core/names.rs b/crates/wasm-encoder/src/core/names.rs
index f3b2fc37..587ae6a7 100644
--- a/crates/wasm-encoder/src/core/names.rs
+++ b/crates/wasm-encoder/src/core/names.rs
@@ -154,6 +154,14 @@ impl NameSection {
         names.encode(&mut self.bytes);
     }
 
+    /// Appends a subsection for the names of all tags in this wasm module.
+    ///
+    /// This section should come after the data name subsection (if present).
+    pub fn tag(&mut self, names: &NameMap) {
+        self.subsection_header(Subsection::Tag, names.size());
+        names.encode(&mut self.bytes);
+    }
+
     /// Appends a subsection for the names of fields within types in this
     /// wasm module.
     ///
diff --git a/crates/wasm-encoder/src/core/types.rs b/crates/wasm-encoder/src/core/types.rs
index 8f47527d..ab4d6594 100644
--- a/crates/wasm-encoder/src/core/types.rs
+++ b/crates/wasm-encoder/src/core/types.rs
@@ -255,6 +255,8 @@ impl ValType {
     pub const FUNCREF: ValType = ValType::Ref(RefType::FUNCREF);
     /// Alias for the `externref` type in WebAssembly
     pub const EXTERNREF: ValType = ValType::Ref(RefType::EXTERNREF);
+    /// Alias for the `exnref` type in WebAssembly
+    pub const EXNREF: ValType = ValType::Ref(RefType::EXNREF);
 }
 
 impl Encode for StorageType {
@@ -304,6 +306,12 @@ impl RefType {
         nullable: true,
         heap_type: HeapType::Extern,
     };
+
+    /// Alias for the `exnref` type in WebAssembly
+    pub const EXNREF: RefType = RefType {
+        nullable: true,
+        heap_type: HeapType::Exn,
+    };
 }
 
 impl Encode for RefType {
@@ -393,6 +401,9 @@ pub enum HeapType {
     /// The unboxed `i31` heap type.
     I31,
 
+    /// The abstract` exception` heap type.
+    Exn,
+
     /// A concrete Wasm-defined type at the given index.
     Concrete(u32),
 }
@@ -410,6 +421,7 @@ impl Encode for HeapType {
             HeapType::Struct => sink.push(0x6B),
             HeapType::Array => sink.push(0x6A),
             HeapType::I31 => sink.push(0x6C),
+            HeapType::Exn => sink.push(0x69),
             // Note that this is encoded as a signed type rather than unsigned
             // as it's decoded as an s33
             HeapType::Concrete(i) => i64::from(*i).encode(sink),
@@ -434,6 +446,7 @@ impl TryFrom<wasmparser::HeapType> for HeapType {
             wasmparser::HeapType::Struct => HeapType::Struct,
             wasmparser::HeapType::Array => HeapType::Array,
             wasmparser::HeapType::I31 => HeapType::I31,
+            wasmparser::HeapType::Exn => HeapType::Exn,
         })
     }
 }
diff --git a/crates/wasm-metadata/src/lib.rs b/crates/wasm-metadata/src/lib.rs
index 53f8ec08..537851be 100644
--- a/crates/wasm-metadata/src/lib.rs
+++ b/crates/wasm-metadata/src/lib.rs
@@ -639,7 +639,8 @@ impl<'a> ModuleNames<'a> {
                 wasmparser::Name::Memory(m) => section.memories(&name_map(&m)?),
                 wasmparser::Name::Global(m) => section.globals(&name_map(&m)?),
                 wasmparser::Name::Element(m) => section.elements(&name_map(&m)?),
-                wasmparser::Name::Data(m) => section.types(&name_map(&m)?),
+                wasmparser::Name::Data(m) => section.data(&name_map(&m)?),
+                wasmparser::Name::Tag(m) => section.tags(&name_map(&m)?),
                 wasmparser::Name::Unknown { .. } => {} // wasm-encoder doesn't support it
             }
         }
diff --git a/crates/wasm-mutate/src/module.rs b/crates/wasm-mutate/src/module.rs
index 536a596d..486c9690 100644
--- a/crates/wasm-mutate/src/module.rs
+++ b/crates/wasm-mutate/src/module.rs
@@ -90,6 +90,7 @@ pub fn map_ref_type(ref_ty: wasmparser::RefType) -> Result<RefType> {
             wasmparser::HeapType::Struct => HeapType::Struct,
             wasmparser::HeapType::Array => HeapType::Array,
             wasmparser::HeapType::I31 => HeapType::I31,
+            wasmparser::HeapType::Exn => HeapType::Exn,
             wasmparser::HeapType::Concrete(i) => HeapType::Concrete(i.as_module_index().unwrap()),
         },
     })
diff --git a/crates/wasm-mutate/src/mutators/translate.rs b/crates/wasm-mutate/src/mutators/translate.rs
index 9a9ba874..ec8e408c 100644
--- a/crates/wasm-mutate/src/mutators/translate.rs
+++ b/crates/wasm-mutate/src/mutators/translate.rs
@@ -210,6 +210,7 @@ pub fn heapty(t: &mut dyn Translator, ty: &wasmparser::HeapType) -> Result<HeapT
         wasmparser::HeapType::Struct => Ok(HeapType::Struct),
         wasmparser::HeapType::Array => Ok(HeapType::Array),
         wasmparser::HeapType::I31 => Ok(HeapType::I31),
+        wasmparser::HeapType::Exn => Ok(HeapType::Exn),
         wasmparser::HeapType::Concrete(i) => Ok(HeapType::Concrete(
             t.remap(Item::Type, i.as_module_index().unwrap())?,
         )),
@@ -374,6 +375,7 @@ pub fn op(t: &mut dyn Translator, op: &Operator<'_>) -> Result<Instruction<'stat
         (map $arg:ident field_index) => (*$arg);
         (map $arg:ident from_type_nullable) => (*$arg);
         (map $arg:ident to_type_nullable) => (*$arg);
+        (map $arg:ident try_table) => ($arg);
 
         // This case takes the arguments of a wasmparser instruction and creates
         // a wasm-encoder instruction. There are a few special cases for where
@@ -386,6 +388,7 @@ pub fn op(t: &mut dyn Translator, op: &Operator<'_>) -> Result<Instruction<'stat
         (build F32Const $arg:ident) => (I::F32Const(f32::from_bits($arg.bits())));
         (build F64Const $arg:ident) => (I::F64Const(f64::from_bits($arg.bits())));
         (build V128Const $arg:ident) => (I::V128Const($arg.i128()));
+        (build TryTable $table:ident) => (unimplemented_try_table());
         (build $op:ident $arg:ident) => (I::$op($arg));
         (build CallIndirect $ty:ident $table:ident $_:ident) => (I::CallIndirect {
             ty: $ty,
@@ -409,6 +412,10 @@ pub fn op(t: &mut dyn Translator, op: &Operator<'_>) -> Result<Instruction<'stat
     wasmparser::for_each_operator!(translate)
 }
 
+fn unimplemented_try_table() -> wasm_encoder::Instruction<'static> {
+    unimplemented!()
+}
+
 pub fn block_type(t: &mut dyn Translator, ty: &wasmparser::BlockType) -> Result<BlockType> {
     match ty {
         wasmparser::BlockType::Empty => Ok(BlockType::Empty),
diff --git a/crates/wasm-smith/src/core/code_builder.rs b/crates/wasm-smith/src/core/code_builder.rs
index b31e1112..56250386 100644
--- a/crates/wasm-smith/src/core/code_builder.rs
+++ b/crates/wasm-smith/src/core/code_builder.rs
@@ -7,7 +7,7 @@ use arbitrary::{Result, Unstructured};
 use std::collections::{BTreeMap, BTreeSet};
 use std::convert::TryFrom;
 use std::rc::Rc;
-use wasm_encoder::{BlockType, ConstExpr, ExportKind, GlobalType, MemArg, RefType};
+use wasm_encoder::{BlockType, Catch, ConstExpr, ExportKind, GlobalType, MemArg, RefType};
 mod no_traps;
 
 macro_rules! instructions {
@@ -94,10 +94,7 @@ instructions! {
     (None, nop, Control, 800),
     (None, block, Control),
     (None, r#loop, Control),
-    (Some(try_valid), r#try, Control),
-    (Some(delegate_valid), delegate, Control),
-    (Some(catch_valid), catch, Control),
-    (Some(catch_all_valid), catch_all, Control),
+    (Some(try_table_valid), try_table, Control),
     (Some(if_valid), r#if, Control),
     (Some(else_valid), r#else, Control),
     (Some(end_valid), end, Control),
@@ -110,7 +107,7 @@ instructions! {
     (Some(return_call_valid), return_call, Control),
     (Some(return_call_indirect_valid), return_call_indirect, Control),
     (Some(throw_valid), throw, Control, 850),
-    (Some(rethrow_valid), rethrow, Control),
+    (Some(throw_ref_valid), throw_ref, Control, 850),
     // Parametric instructions.
     (Some(drop_valid), drop, Parametric, 990),
     (Some(select_valid), select, Parametric),
@@ -662,9 +659,7 @@ enum ControlKind {
     Block,
     If,
     Loop,
-    Try,
-    Catch,
-    CatchAll,
+    TryTable,
 }
 
 enum Float {
@@ -1306,114 +1301,69 @@ fn block(
 }
 
 #[inline]
-fn try_valid(module: &Module, _: &mut CodeBuilder) -> bool {
+fn try_table_valid(module: &Module, _: &mut CodeBuilder) -> bool {
     module.config.exceptions_enabled
 }
 
-fn r#try(
+fn try_table<'m>(
     u: &mut Unstructured,
     module: &Module,
-    builder: &mut CodeBuilder,
+    builder: &'m mut CodeBuilder,
     instructions: &mut Vec<Instruction>,
 ) -> Result<()> {
     let block_ty = builder.arbitrary_block_type(u, module)?;
-    let (params, results) = module.params_results(&block_ty);
-    let height = builder.allocs.operands.len() - params.len();
-    builder.allocs.controls.push(Control {
-        kind: ControlKind::Try,
-        params,
-        results,
-        height,
-    });
-    instructions.push(Instruction::Try(block_ty));
-    Ok(())
-}
 
-#[inline]
-fn delegate_valid(module: &Module, builder: &mut CodeBuilder) -> bool {
-    let control_kind = builder.allocs.controls.last().unwrap().kind;
-    // delegate is only valid if end could be used in a try control frame
-    module.config.exceptions_enabled
-        && control_kind == ControlKind::Try
-        && end_valid(module, builder)
-}
+    let mut catch_options: Vec<Box<dyn Fn(&mut Unstructured<'_>) -> Result<Catch> + 'm>> =
+        Vec::new();
 
-fn delegate(
-    u: &mut Unstructured,
-    _: &Module,
-    builder: &mut CodeBuilder,
-    instructions: &mut Vec<Instruction>,
-) -> Result<()> {
-    // There will always be at least the function's return frame and try
-    // control frame if we are emitting delegate
-    let n = builder.allocs.controls.iter().count();
-    debug_assert!(n >= 2);
-    // Delegate must target an outer control from the try block, and is
-    // encoded with relative depth from the outer control
-    let target_relative_from_last = u.int_in_range(1..=n - 1)?;
-    let target_relative_from_outer = target_relative_from_last - 1;
-    // Delegate ends the try block
-    builder.allocs.controls.pop();
-    instructions.push(Instruction::Delegate(target_relative_from_outer as u32));
-    Ok(())
-}
+    for (i, ctrl) in builder.allocs.controls.iter().rev().enumerate() {
+        let i = i as u32;
 
-#[inline]
-fn catch_valid(module: &Module, builder: &mut CodeBuilder) -> bool {
-    let control_kind = builder.allocs.controls.last().unwrap().kind;
-    // catch is only valid if end could be used in a try or catch (not
-    // catch_all) control frame. There must also be a tag that we can catch.
-    module.config.exceptions_enabled
-        && (control_kind == ControlKind::Try || control_kind == ControlKind::Catch)
-        && end_valid(module, builder)
-        && module.tags.len() > 0
-}
+        let label_types = ctrl.label_types();
+        if label_types.is_empty() {
+            catch_options.push(Box::new(move |_| Ok(Catch::All { label: i })));
+        }
+        if label_types == [ValType::EXNREF] {
+            catch_options.push(Box::new(move |_| Ok(Catch::AllRef { label: i })));
+        }
 
-fn catch(
-    u: &mut Unstructured,
-    module: &Module,
-    builder: &mut CodeBuilder,
-    instructions: &mut Vec<Instruction>,
-) -> Result<()> {
-    let tag_idx = u.int_in_range(0..=(module.tags.len() - 1))?;
-    let tag_type = &module.tags[tag_idx];
-    let control = builder.allocs.controls.pop().unwrap();
-    // Pop the results for the previous try or catch
-    builder.pop_operands(&control.results);
-    // Push the params of the tag we're catching
-    builder.push_operands(&tag_type.func_type.params);
-    builder.allocs.controls.push(Control {
-        kind: ControlKind::Catch,
-        ..control
-    });
-    instructions.push(Instruction::Catch(tag_idx as u32));
-    Ok(())
-}
+        if let Some(tags) = builder.allocs.tags.get(label_types) {
+            catch_options.push(Box::new(move |u| {
+                Ok(Catch::One {
+                    tag: *u.choose(&tags)?,
+                    label: i,
+                })
+            }));
+        }
 
-#[inline]
-fn catch_all_valid(module: &Module, builder: &mut CodeBuilder) -> bool {
-    let control_kind = builder.allocs.controls.last().unwrap().kind;
-    // catch_all is only valid if end could be used in a try or catch (not
-    // catch_all) control frame.
-    module.config.exceptions_enabled
-        && (control_kind == ControlKind::Try || control_kind == ControlKind::Catch)
-        && end_valid(module, builder)
-}
+        let mut label_types_with_exnref = label_types.to_vec();
+        label_types_with_exnref.push(ValType::EXNREF);
+        if let Some(tags) = builder.allocs.tags.get(&label_types_with_exnref) {
+            catch_options.push(Box::new(move |u| {
+                Ok(Catch::OneRef {
+                    tag: *u.choose(&tags)?,
+                    label: i,
+                })
+            }));
+        }
+    }
 
-fn catch_all(
-    _: &mut Unstructured,
-    _: &Module,
-    builder: &mut CodeBuilder,
-    instructions: &mut Vec<Instruction>,
-) -> Result<()> {
-    let control = builder.allocs.controls.pop().unwrap();
-    // Pop the results for the previous try or catch
-    builder.pop_operands(&control.results);
+    let mut catches = Vec::new();
+    if catch_options.len() > 0 {
+        for _ in 0..u.int_in_range(0..=10)? {
+            catches.push(u.choose(&mut catch_options)?(u)?);
+        }
+    }
+
+    let (params, results) = module.params_results(&block_ty);
+    let height = builder.allocs.operands.len() - params.len();
     builder.allocs.controls.push(Control {
-        kind: ControlKind::CatchAll,
-        ..control
+        kind: ControlKind::TryTable,
+        params,
+        results,
+        height,
     });
-    instructions.push(Instruction::CatchAll);
+    instructions.push(Instruction::TryTable(block_ty, catches.into()));
     Ok(())
 }
 
@@ -1863,40 +1813,18 @@ fn throw(
 }
 
 #[inline]
-fn rethrow_valid(module: &Module, builder: &mut CodeBuilder) -> bool {
-    // There must be a catch or catch_all control on the stack
-    module.config.exceptions_enabled
-        && builder
-            .allocs
-            .controls
-            .iter()
-            .any(|l| l.kind == ControlKind::Catch || l.kind == ControlKind::CatchAll)
+fn throw_ref_valid(module: &Module, builder: &mut CodeBuilder) -> bool {
+    module.config.exceptions_enabled && builder.types_on_stack(&[ValType::EXNREF])
 }
 
-fn rethrow(
-    u: &mut Unstructured,
-    _: &Module,
+fn throw_ref(
+    _u: &mut Unstructured,
+    _module: &Module,
     builder: &mut CodeBuilder,
     instructions: &mut Vec<Instruction>,
 ) -> Result<()> {
-    let n = builder
-        .allocs
-        .controls
-        .iter()
-        .filter(|l| l.kind == ControlKind::Catch || l.kind == ControlKind::CatchAll)
-        .count();
-    debug_assert!(n > 0);
-    let i = u.int_in_range(0..=n - 1)?;
-    let (target, _) = builder
-        .allocs
-        .controls
-        .iter()
-        .rev()
-        .enumerate()
-        .filter(|(_, l)| l.kind == ControlKind::Catch || l.kind == ControlKind::CatchAll)
-        .nth(i)
-        .unwrap();
-    instructions.push(Instruction::Rethrow(target as u32));
+    builder.pop_operands(&[ValType::EXNREF]);
+    instructions.push(Instruction::ThrowRef);
     Ok(())
 }
 
diff --git a/crates/wasmparser/src/binary_reader.rs b/crates/wasmparser/src/binary_reader.rs
index 5cbc94ea..032402fe 100644
--- a/crates/wasmparser/src/binary_reader.rs
+++ b/crates/wasmparser/src/binary_reader.rs
@@ -681,7 +681,7 @@ impl<'a> BinaryReader<'a> {
         Ok(self.buffer[self.position])
     }
 
-    fn read_block_type(&mut self) -> Result<BlockType> {
+    pub(crate) fn read_block_type(&mut self) -> Result<BlockType> {
         let b = self.peek()?;
 
         // Check for empty block
@@ -770,6 +770,7 @@ impl<'a> BinaryReader<'a> {
             0x07 => visitor.visit_catch(self.read_var_u32()?),
             0x08 => visitor.visit_throw(self.read_var_u32()?),
             0x09 => visitor.visit_rethrow(self.read_var_u32()?),
+            0x0a => visitor.visit_throw_ref(),
             0x0b => visitor.visit_end(),
             0x0c => visitor.visit_br(self.read_var_u32()?),
             0x0d => visitor.visit_br_if(self.read_var_u32()?),
@@ -799,6 +800,7 @@ impl<'a> BinaryReader<'a> {
                 }
                 visitor.visit_typed_select(self.read()?)
             }
+            0x1f => visitor.visit_try_table(self.read()?),
 
             0x20 => visitor.visit_local_get(self.read_var_u32()?),
             0x21 => visitor.visit_local_set(self.read_var_u32()?),
diff --git a/crates/wasmparser/src/lib.rs b/crates/wasmparser/src/lib.rs
index e10f5e9d..6b3aed5c 100644
--- a/crates/wasmparser/src/lib.rs
+++ b/crates/wasmparser/src/lib.rs
@@ -129,10 +129,15 @@ macro_rules! for_each_operator {
             @mvp Loop { blockty: $crate::BlockType } => visit_loop
             @mvp If { blockty: $crate::BlockType } => visit_if
             @mvp Else => visit_else
+            @exceptions TryTable { try_table: $crate::TryTable } => visit_try_table
+            @exceptions Throw { tag_index: u32 } => visit_throw
+            @exceptions ThrowRef => visit_throw_ref
+            // Deprecated old instructions from the exceptions proposal
             @exceptions Try { blockty: $crate::BlockType } => visit_try
             @exceptions Catch { tag_index: u32 } => visit_catch
-            @exceptions Throw { tag_index: u32 } => visit_throw
             @exceptions Rethrow { relative_depth: u32 } => visit_rethrow
+            @exceptions Delegate { relative_depth: u32 } => visit_delegate
+            @exceptions CatchAll => visit_catch_all
             @mvp End => visit_end
             @mvp Br { relative_depth: u32 } => visit_br
             @mvp BrIf { relative_depth: u32 } => visit_br_if
@@ -142,8 +147,6 @@ macro_rules! for_each_operator {
             @mvp CallIndirect { type_index: u32, table_index: u32, table_byte: u8 } => visit_call_indirect
             @tail_call ReturnCall { function_index: u32 } => visit_return_call
             @tail_call ReturnCallIndirect { type_index: u32, table_index: u32 } => visit_return_call_indirect
-            @exceptions Delegate { relative_depth: u32 } => visit_delegate
-            @exceptions CatchAll => visit_catch_all
             @mvp Drop => visit_drop
             @mvp Select => visit_select
             @reference_types TypedSelect { ty: $crate::ValType } => visit_typed_select
diff --git a/crates/wasmparser/src/limits.rs b/crates/wasmparser/src/limits.rs
index 55acf53e..9cebf27b 100644
--- a/crates/wasmparser/src/limits.rs
+++ b/crates/wasmparser/src/limits.rs
@@ -36,6 +36,7 @@ pub const MAX_WASM_MEMORIES: usize = 100;
 pub const MAX_WASM_TAGS: usize = 1_000_000;
 pub const MAX_WASM_BR_TABLE_SIZE: usize = MAX_WASM_FUNCTION_SIZE;
 pub const MAX_WASM_STRUCT_FIELDS: usize = 10_000;
+pub const MAX_WASM_CATCHES: usize = 10_000;
 
 // Component-related limits
 pub const MAX_WASM_MODULE_SIZE: usize = 1024 * 1024 * 1024; //= 1 GiB
diff --git a/crates/wasmparser/src/readers/core/names.rs b/crates/wasmparser/src/readers/core/names.rs
index aa8a11dd..b4782427 100644
--- a/crates/wasmparser/src/readers/core/names.rs
+++ b/crates/wasmparser/src/readers/core/names.rs
@@ -101,6 +101,8 @@ pub enum Name<'a> {
     Element(NameMap<'a>),
     /// The name is for the data segments.
     Data(NameMap<'a>),
+    /// The name is for tags.
+    Tag(NameMap<'a>),
     /// An unknown [name subsection](https://webassembly.github.io/spec/core/appendix/custom.html#subsections).
     Unknown {
         /// The identifier for this subsection.
@@ -143,6 +145,7 @@ impl<'a> Subsection<'a> for Name<'a> {
             7 => Name::Global(NameMap::new(data, offset)?),
             8 => Name::Element(NameMap::new(data, offset)?),
             9 => Name::Data(NameMap::new(data, offset)?),
+            11 => Name::Tag(NameMap::new(data, offset)?),
             ty => Name::Unknown {
                 ty,
                 data,
diff --git a/crates/wasmparser/src/readers/core/operators.rs b/crates/wasmparser/src/readers/core/operators.rs
index d1312c25..004982e7 100644
--- a/crates/wasmparser/src/readers/core/operators.rs
+++ b/crates/wasmparser/src/readers/core/operators.rs
@@ -13,7 +13,8 @@
  * limitations under the License.
  */
 
-use crate::{BinaryReader, BinaryReaderError, Result, ValType};
+use crate::limits::MAX_WASM_CATCHES;
+use crate::{BinaryReader, BinaryReaderError, FromReader, Result, ValType};
 
 /// Represents a block type.
 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
@@ -352,3 +353,59 @@ impl<'a, V: VisitOperator<'a> + ?Sized> VisitOperator<'a> for Box<V> {
     }
     for_each_operator!(define_visit_operator_delegate);
 }
+
+/// A `try_table` entries representation.
+#[derive(Clone, Debug)]
+pub struct TryTable {
+    /// The block type describing the try block itself.
+    pub ty: BlockType,
+    /// Outer blocks which will receive exceptions.
+    pub catches: Vec<Catch>,
+}
+
+/// Catch clauses that can be specified in [`TryTable`].
+#[derive(Copy, Clone, Debug)]
+#[allow(missing_docs)]
+pub enum Catch {
+    /// Equivalent of `catch`
+    One { tag: u32, label: u32 },
+    /// Equivalent of `catch_ref`
+    OneRef { tag: u32, label: u32 },
+    /// Equivalent of `catch_all`
+    All { label: u32 },
+    /// Equivalent of `catch_all_ref`
+    AllRef { label: u32 },
+}
+
+impl<'a> FromReader<'a> for TryTable {
+    fn from_reader(reader: &mut BinaryReader<'a>) -> Result<Self> {
+        let ty = reader.read_block_type()?;
+        let catches = reader
+            .read_iter(MAX_WASM_CATCHES, "catches")?
+            .collect::<Result<_>>()?;
+        Ok(TryTable { ty, catches })
+    }
+}
+
+impl<'a> FromReader<'a> for Catch {
+    fn from_reader(reader: &mut BinaryReader<'a>) -> Result<Self> {
+        Ok(match reader.read_u8()? {
+            0x00 => Catch::One {
+                tag: reader.read_var_u32()?,
+                label: reader.read_var_u32()?,
+            },
+            0x01 => Catch::OneRef {
+                tag: reader.read_var_u32()?,
+                label: reader.read_var_u32()?,
+            },
+            0x02 => Catch::All {
+                label: reader.read_var_u32()?,
+            },
+            0x03 => Catch::AllRef {
+                label: reader.read_var_u32()?,
+            },
+
+            x => return reader.invalid_leading_byte(x, "catch"),
+        })
+    }
+}
diff --git a/crates/wasmparser/src/readers/core/types.rs b/crates/wasmparser/src/readers/core/types.rs
index eda7f93f..6845426d 100644
--- a/crates/wasmparser/src/readers/core/types.rs
+++ b/crates/wasmparser/src/readers/core/types.rs
@@ -738,6 +738,9 @@ impl ValType {
     /// Alias for the wasm `externref` type.
     pub const EXTERNREF: ValType = ValType::Ref(RefType::EXTERNREF);
 
+    /// Alias for the wasm `exnref` type.
+    pub const EXNREF: ValType = ValType::Ref(RefType::EXNREF);
+
     /// Returns whether this value type is a "reference type".
     ///
     /// Only reference types are allowed in tables, for example, and with some
@@ -829,6 +832,8 @@ impl ValType {
 //   0011 = extern
 //   0010 = noextern
 //
+//   0001 = exn
+//
 //   0000 = none
 //   ```
 #[derive(Copy, Clone, PartialEq, Eq, Hash)]
@@ -857,6 +862,8 @@ impl std::fmt::Debug for RefType {
             (false, HeapType::Extern) => write!(f, "(ref extern)"),
             (true, HeapType::Func) => write!(f, "funcref"),
             (false, HeapType::Func) => write!(f, "(ref func)"),
+            (true, HeapType::Exn) => write!(f, "exnref"),
+            (false, HeapType::Exn) => write!(f, "(ref exn)"),
             (true, HeapType::Concrete(idx)) => write!(f, "(ref null {idx})"),
             (false, HeapType::Concrete(idx)) => write!(f, "(ref {idx})"),
         }
@@ -906,6 +913,7 @@ impl RefType {
     const NOFUNC_ABSTYPE: u32 = 0b0100 << 18;
     const EXTERN_ABSTYPE: u32 = 0b0011 << 18;
     const NOEXTERN_ABSTYPE: u32 = 0b0010 << 18;
+    const EXN_ABSTYPE: u32 = 0b0001 << 18;
     const NONE_ABSTYPE: u32 = 0b0000 << 18;
 
     // The `index` is valid only when `concrete == 1`.
@@ -919,6 +927,10 @@ impl RefType {
     /// `externref`.
     pub const EXTERNREF: Self = RefType::EXTERN.nullable();
 
+    /// A nullable reference to an exception object aka `(ref null exn)` aka
+    /// `exnref`.
+    pub const EXNREF: Self = RefType::EXN.nullable();
+
     /// A non-nullable untyped function reference aka `(ref func)`.
     pub const FUNC: Self = RefType::from_u32(Self::FUNC_ABSTYPE);
 
@@ -949,6 +961,9 @@ impl RefType {
     /// A non-nullable reference to an i31 object aka `(ref i31)`.
     pub const I31: Self = RefType::from_u32(Self::I31_ABSTYPE);
 
+    /// A non-nullable reference to an exn object aka `(ref exn)`.
+    pub const EXN: Self = RefType::from_u32(Self::EXN_ABSTYPE);
+
     const fn can_represent_type_index(index: u32) -> bool {
         index & Self::INDEX_MASK == index
     }
@@ -988,6 +1003,7 @@ impl RefType {
                         | Self::EXTERN_ABSTYPE
                         | Self::NOEXTERN_ABSTYPE
                         | Self::NONE_ABSTYPE
+                        | Self::EXN_ABSTYPE
                 )
         );
 
@@ -1025,6 +1041,7 @@ impl RefType {
             HeapType::Struct => Some(Self::from_u32(nullable32 | Self::STRUCT_ABSTYPE)),
             HeapType::Array => Some(Self::from_u32(nullable32 | Self::ARRAY_ABSTYPE)),
             HeapType::I31 => Some(Self::from_u32(nullable32 | Self::I31_ABSTYPE)),
+            HeapType::Exn => Some(Self::from_u32(nullable32 | Self::EXN_ABSTYPE)),
         }
     }
 
@@ -1120,6 +1137,7 @@ impl RefType {
                 Self::STRUCT_ABSTYPE => HeapType::Struct,
                 Self::ARRAY_ABSTYPE => HeapType::Array,
                 Self::I31_ABSTYPE => HeapType::I31,
+                Self::EXN_ABSTYPE => HeapType::Exn,
                 _ => unreachable!(),
             }
         }
@@ -1140,6 +1158,7 @@ impl RefType {
             (true, HeapType::Struct) => "structref",
             (true, HeapType::Array) => "arrayref",
             (true, HeapType::I31) => "i31ref",
+            (true, HeapType::Exn) => "exnref",
             (false, HeapType::Func) => "(ref func)",
             (false, HeapType::Extern) => "(ref extern)",
             (false, HeapType::Concrete(_)) => "(ref $type)",
@@ -1151,6 +1170,7 @@ impl RefType {
             (false, HeapType::Struct) => "(ref struct)",
             (false, HeapType::Array) => "(ref array)",
             (false, HeapType::I31) => "(ref i31)",
+            (false, HeapType::Exn) => "(ref exn)",
         }
     }
 }
@@ -1231,13 +1251,18 @@ pub enum HeapType {
     ///
     /// Introduced in the GC proposal.
     I31,
+
+    /// The abstraction `exception` heap type.
+    ///
+    /// Introduced in the exception-handling proposal.
+    Exn,
 }
 
 impl ValType {
     pub(crate) fn is_valtype_byte(byte: u8) -> bool {
         match byte {
             0x7F | 0x7E | 0x7D | 0x7C | 0x7B | 0x70 | 0x6F | 0x64 | 0x63 | 0x6E | 0x71 | 0x72
-            | 0x73 | 0x6D | 0x6B | 0x6A | 0x6C => true,
+            | 0x73 | 0x6D | 0x6B | 0x6A | 0x6C | 0x69 => true,
             _ => false,
         }
     }
@@ -1282,9 +1307,8 @@ impl<'a> FromReader<'a> for ValType {
                 reader.position += 1;
                 Ok(ValType::V128)
             }
-            0x70 | 0x6F | 0x64 | 0x63 | 0x6E | 0x71 | 0x72 | 0x73 | 0x6D | 0x6B | 0x6A | 0x6C => {
-                Ok(ValType::Ref(reader.read()?))
-            }
+            0x70 | 0x6F | 0x64 | 0x63 | 0x6E | 0x71 | 0x72 | 0x73 | 0x6D | 0x6B | 0x6A | 0x6C
+            | 0x69 => Ok(ValType::Ref(reader.read()?)),
             _ => bail!(reader.original_position(), "invalid value type"),
         }
     }
@@ -1303,6 +1327,7 @@ impl<'a> FromReader<'a> for RefType {
             0x6B => Ok(RefType::STRUCT.nullable()),
             0x6A => Ok(RefType::ARRAY.nullable()),
             0x6C => Ok(RefType::I31.nullable()),
+            0x69 => Ok(RefType::EXN.nullable()),
             byte @ (0x63 | 0x64) => {
                 let nullable = byte == 0x63;
                 let pos = reader.original_position();
@@ -1357,6 +1382,10 @@ impl<'a> FromReader<'a> for HeapType {
                 reader.position += 1;
                 Ok(HeapType::I31)
             }
+            0x69 => {
+                reader.position += 1;
+                Ok(HeapType::Exn)
+            }
             _ => {
                 let idx = match u32::try_from(reader.read_var_s33()?) {
                     Ok(idx) => idx,
diff --git a/crates/wasmparser/src/validator.rs b/crates/wasmparser/src/validator.rs
index e6309354..23c1899b 100644
--- a/crates/wasmparser/src/validator.rs
+++ b/crates/wasmparser/src/validator.rs
@@ -352,6 +352,15 @@ impl WasmFeatures {
                     Err("heap types not supported without the gc feature")
                 }
             }
+
+            // These types were added in the exception-handling proposal.
+            (HeapType::Exn, _) => {
+                if self.exceptions {
+                    Ok(())
+                } else {
+                    Err("exception refs not supported without the exception handling feature")
+                }
+            }
         }
     }
 }
diff --git a/crates/wasmparser/src/validator/core.rs b/crates/wasmparser/src/validator/core.rs
index a539de5e..700adac3 100644
--- a/crates/wasmparser/src/validator/core.rs
+++ b/crates/wasmparser/src/validator/core.rs
@@ -981,7 +981,8 @@ impl Module {
             | HeapType::Eq
             | HeapType::Struct
             | HeapType::Array
-            | HeapType::I31 => return Ok(()),
+            | HeapType::I31
+            | HeapType::Exn => return Ok(()),
             HeapType::Concrete(type_index) => type_index,
         };
         match type_index {
diff --git a/crates/wasmparser/src/validator/operators.rs b/crates/wasmparser/src/validator/operators.rs
index eb285c31..a8cd9c04 100644
--- a/crates/wasmparser/src/validator/operators.rs
+++ b/crates/wasmparser/src/validator/operators.rs
@@ -23,10 +23,10 @@
 // the various methods here.
 
 use crate::{
-    limits::MAX_WASM_FUNCTION_LOCALS, ArrayType, BinaryReaderError, BlockType, BrTable,
+    limits::MAX_WASM_FUNCTION_LOCALS, ArrayType, BinaryReaderError, BlockType, BrTable, Catch,
     CompositeType, FieldType, FuncType, HeapType, Ieee32, Ieee64, MemArg, RefType, Result,
-    StorageType, StructType, SubType, UnpackedIndex, ValType, VisitOperator, WasmFeatures,
-    WasmModuleResources, V128,
+    StorageType, StructType, SubType, TryTable, UnpackedIndex, ValType, VisitOperator,
+    WasmFeatures, WasmModuleResources, V128,
 };
 use std::ops::{Deref, DerefMut};
 
@@ -118,19 +118,7 @@ pub enum FrameKind {
     /// # Note
     ///
     /// This belongs to the Wasm exception handling proposal.
-    Try,
-    /// A Wasm `catch` control block.
-    ///
-    /// # Note
-    ///
-    /// This belongs to the Wasm exception handling proposal.
-    Catch,
-    /// A Wasm `catch_all` control block.
-    ///
-    /// # Note
-    ///
-    /// This belongs to the Wasm exception handling proposal.
-    CatchAll,
+    TryTable,
 }
 
 struct OperatorValidatorTemp<'validator, 'resources, T> {
@@ -1295,34 +1283,74 @@ where
         self.push_ctrl(FrameKind::Else, frame.block_type)?;
         Ok(())
     }
-    fn visit_try(&mut self, mut ty: BlockType) -> Self::Output {
-        self.check_block_type(&mut ty)?;
-        for ty in self.params(ty)?.rev() {
+    fn visit_try_table(&mut self, mut ty: TryTable) -> Self::Output {
+        self.check_block_type(&mut ty.ty)?;
+        for ty in self.params(ty.ty)?.rev() {
             self.pop_operand(Some(ty))?;
         }
-        self.push_ctrl(FrameKind::Try, ty)?;
-        Ok(())
-    }
-    fn visit_catch(&mut self, index: u32) -> Self::Output {
-        let frame = self.pop_ctrl()?;
-        if frame.kind != FrameKind::Try && frame.kind != FrameKind::Catch {
-            bail!(self.offset, "catch found outside of an `try` block");
-        }
-        // Start a new frame and push `exnref` value.
-        let height = self.operands.len();
-        let init_height = self.inits.len();
-        self.control.push(Frame {
-            kind: FrameKind::Catch,
-            block_type: frame.block_type,
-            height,
-            unreachable: false,
-            init_height,
-        });
-        // Push exception argument types.
-        let ty = self.tag_at(index)?;
-        for ty in ty.params() {
-            self.push_operand(*ty)?;
+        for catch in ty.catches {
+            match catch {
+                Catch::One { tag, label } => {
+                    let tag = self.tag_at(tag)?;
+                    let (ty, kind) = self.jump(label)?;
+                    let params = tag.params();
+                    let types = self.label_types(ty, kind)?;
+                    if params.len() != types.len() {
+                        bail!(
+                            self.offset,
+                            "type mismatch: catch label must have same number of types as tag"
+                        );
+                    }
+                    for (expected, actual) in types.zip(params) {
+                        self.push_operand(*actual)?;
+                        self.pop_operand(Some(expected))?;
+                    }
+                }
+                Catch::OneRef { tag, label } => {
+                    let tag = self.tag_at(tag)?;
+                    let (ty, kind) = self.jump(label)?;
+                    let params = tag.params().iter().copied();
+                    let types = self.label_types(ty, kind)?;
+                    if params.len() + 1 != types.len() {
+                        bail!(
+                            self.offset,
+                            "type mismatch: catch_ref label must have one \
+                             more type than tag types",
+                        );
+                    }
+                    for (expected, actual) in types.zip(params.chain([ValType::EXNREF])) {
+                        self.push_operand(actual)?;
+                        self.pop_operand(Some(expected))?;
+                    }
+                }
+
+                Catch::All { label } => {
+                    let (ty, kind) = self.jump(label)?;
+                    if self.label_types(ty, kind)?.len() != 0 {
+                        bail!(
+                            self.offset,
+                            "type mismatch: catch_all label must have no result types"
+                        );
+                    }
+                }
+
+                Catch::AllRef { label } => {
+                    let (ty, kind) = self.jump(label)?;
+                    let mut types = self.label_types(ty, kind)?;
+                    match (types.next(), types.next()) {
+                        (Some(ValType::EXNREF), None) => {}
+                        _ => {
+                            bail!(
+                                self.offset,
+                                "type mismatch: catch_all_ref label must have \
+                                 one exnref result type"
+                            );
+                        }
+                    }
+                }
+            }
         }
+        self.push_ctrl(FrameKind::TryTable, ty.ty)?;
         Ok(())
     }
     fn visit_throw(&mut self, index: u32) -> Self::Output {
@@ -1340,49 +1368,25 @@ where
         self.unreachable()?;
         Ok(())
     }
-    fn visit_rethrow(&mut self, relative_depth: u32) -> Self::Output {
-        // This is not a jump, but we need to check that the `rethrow`
-        // targets an actual `catch` to get the exception.
-        let (_, kind) = self.jump(relative_depth)?;
-        if kind != FrameKind::Catch && kind != FrameKind::CatchAll {
-            bail!(
-                self.offset,
-                "invalid rethrow label: target was not a `catch` block"
-            );
-        }
+    fn visit_throw_ref(&mut self) -> Self::Output {
+        self.pop_operand(Some(ValType::EXNREF))?;
         self.unreachable()?;
         Ok(())
     }
-    fn visit_delegate(&mut self, relative_depth: u32) -> Self::Output {
-        let frame = self.pop_ctrl()?;
-        if frame.kind != FrameKind::Try {
-            bail!(self.offset, "delegate found outside of an `try` block");
-        }
-        // This operation is not a jump, but we need to check the
-        // depth for validity
-        let _ = self.jump(relative_depth)?;
-        for ty in self.results(frame.block_type)? {
-            self.push_operand(ty)?;
-        }
-        Ok(())
+    fn visit_try(&mut self, _: BlockType) -> Self::Output {
+        bail!(self.offset, "unimplemented validation of deprecated opcode")
+    }
+    fn visit_catch(&mut self, _: u32) -> Self::Output {
+        bail!(self.offset, "unimplemented validation of deprecated opcode")
+    }
+    fn visit_rethrow(&mut self, _: u32) -> Self::Output {
+        bail!(self.offset, "unimplemented validation of deprecated opcode")
+    }
+    fn visit_delegate(&mut self, _: u32) -> Self::Output {
+        bail!(self.offset, "unimplemented validation of deprecated opcode")
     }
     fn visit_catch_all(&mut self) -> Self::Output {
-        let frame = self.pop_ctrl()?;
-        if frame.kind == FrameKind::CatchAll {
-            bail!(self.offset, "only one catch_all allowed per `try` block");
-        } else if frame.kind != FrameKind::Try && frame.kind != FrameKind::Catch {
-            bail!(self.offset, "catch_all found outside of a `try` block");
-        }
-        let height = self.operands.len();
-        let init_height = self.inits.len();
-        self.control.push(Frame {
-            kind: FrameKind::CatchAll,
-            block_type: frame.block_type,
-            height,
-            unreachable: false,
-            init_height,
-        });
-        Ok(())
+        bail!(self.offset, "unimplemented validation of deprecated opcode")
     }
     fn visit_end(&mut self) -> Self::Output {
         let mut frame = self.pop_ctrl()?;
diff --git a/crates/wasmparser/src/validator/types.rs b/crates/wasmparser/src/validator/types.rs
index a893acb6..acab322d 100644
--- a/crates/wasmparser/src/validator/types.rs
+++ b/crates/wasmparser/src/validator/types.rs
@@ -2708,6 +2708,10 @@ impl TypeList {
             | (HT::Struct, _)
             | (HT::Array, _)
             | (HT::I31, _) => false,
+
+            // TODO: this probably isn't right, this is probably related to some
+            // gc type.
+            (HT::Exn, _) => false,
         }
     }
 
@@ -2745,6 +2749,7 @@ impl TypeList {
             | HeapType::Array
             | HeapType::I31
             | HeapType::None => HeapType::Any,
+            HeapType::Exn => HeapType::Exn,
         }
     }
 
diff --git a/crates/wasmprinter/src/lib.rs b/crates/wasmprinter/src/lib.rs
index e011f842..0832471a 100644
--- a/crates/wasmprinter/src/lib.rs
+++ b/crates/wasmprinter/src/lib.rs
@@ -68,6 +68,7 @@ struct CoreState {
     local_names: HashMap<(u32, u32), Naming>,
     label_names: HashMap<(u32, u32), Naming>,
     type_names: HashMap<u32, Naming>,
+    tag_names: HashMap<u32, Naming>,
     table_names: HashMap<u32, Naming>,
     memory_names: HashMap<u32, Naming>,
     global_names: HashMap<u32, Naming>,
@@ -579,6 +580,7 @@ impl Printer {
                 Name::Global(n) => name_map(&mut state.core.global_names, n, "global")?,
                 Name::Element(n) => name_map(&mut state.core.element_names, n, "elem")?,
                 Name::Data(n) => name_map(&mut state.core.data_names, n, "data")?,
+                Name::Tag(n) => name_map(&mut state.core.tag_names, n, "tag")?,
                 Name::Unknown { .. } => (),
             }
         }
@@ -886,6 +888,7 @@ impl Printer {
                 RefType::EQ => self.result.push_str("eqref"),
                 RefType::STRUCT => self.result.push_str("structref"),
                 RefType::ARRAY => self.result.push_str("arrayref"),
+                RefType::EXN => self.result.push_str("exnref"),
                 _ => {
                     self.result.push_str("(ref null ");
                     self.print_heaptype(ty.heap_type())?;
@@ -912,6 +915,7 @@ impl Printer {
             HeapType::Struct => self.result.push_str("struct"),
             HeapType::Array => self.result.push_str("array"),
             HeapType::I31 => self.result.push_str("i31"),
+            HeapType::Exn => self.result.push_str("exn"),
             HeapType::Concrete(i) => self
                 .result
                 .push_str(&format!("{}", i.as_module_index().unwrap())),
@@ -996,7 +1000,8 @@ impl Printer {
     fn print_tag_type(&mut self, state: &State, ty: &TagType, index: bool) -> Result<()> {
         self.start_group("tag ");
         if index {
-            write!(self.result, "(;{};) ", state.core.tags)?;
+            self.print_name(&state.core.tag_names, state.core.tags)?;
+            self.result.push(' ');
         }
         self.print_core_functype_idx(state, ty.func_type_idx, None)?;
         Ok(())
@@ -1187,7 +1192,7 @@ impl Printer {
                 }
 
                 // Exiting a block prints `end` at the previous indentation
-                // level. `delegate` also ends a block like `end` for `try`.
+                // level.
                 operator::OpKind::End | operator::OpKind::Delegate
                     if op_printer.printer.nesting > nesting_start =>
                 {
@@ -2446,7 +2451,7 @@ impl Printer {
                     }
                     ExternalKind::Tag => {
                         self.start_group("core tag ");
-                        write!(self.result, "(;{};)", state.core.tags)?;
+                        self.print_name(&state.core.tag_names, state.core.tags)?;
                         self.end_group();
                         state.core.tags += 1;
                     }
diff --git a/crates/wasmprinter/src/operator.rs b/crates/wasmprinter/src/operator.rs
index 3d17480e..ff7c2525 100644
--- a/crates/wasmprinter/src/operator.rs
+++ b/crates/wasmprinter/src/operator.rs
@@ -1,7 +1,7 @@
 use super::{Printer, State};
 use anyhow::{anyhow, bail, Result};
 use std::fmt::Write;
-use wasmparser::{BlockType, BrTable, MemArg, RefType, VisitOperator};
+use wasmparser::{BlockType, BrTable, Catch, MemArg, RefType, TryTable, VisitOperator};
 
 pub struct PrintOperator<'a, 'b> {
     pub(super) printer: &'a mut Printer,
@@ -43,41 +43,34 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
 
             // The previous label is being defined at the same depth as the
             // latest label, meaning it's overwriting its entry.
-            OpKind::BlockMid | OpKind::Delegate => {
+            OpKind::BlockMid => {
                 if let Some(last) = self.label_indices.last_mut() {
                     *last = self.label - 1;
                 }
             }
 
             // Label is out of scope so remove it from the stack.
-            OpKind::End => {
+            OpKind::End | OpKind::Delegate => {
                 self.label_indices.pop();
             }
         }
     }
 
     fn blockty(&mut self, ty: BlockType) -> Result<()> {
-        // This boolean is a little unfortunate. The block type is a payload on
-        // all instructions so printing prints the instruction name, then a
-        // space, then calls this method. We're guaranteed to print something
-        // here, but it's a bit confusing as to when depending on whether this
-        // block is named or not. If this block is named then we'll print the
-        // name and then optionally the type. If the block isn't named then
-        // we'll print the type and then a comment with a pseudo-name. The type
-        // may or may not print something.
-        //
-        // Add all that up and the best way I can use to keep track of this and
-        // not emit trailing whitespace at the end of a line is this boolean.
-        let mut preceding_space = true;
+        let has_name = self.blockty_without_label_comment(ty)?;
+        self.maybe_blockty_label_comment(has_name)
+    }
+
+    fn blockty_without_label_comment(&mut self, ty: BlockType) -> Result<bool> {
+        // Trim the trailing space, if any.
+        if self.result().ends_with(" ") {
+            self.result().pop();
+        }
 
-        let has_name = if let Some(name) = self
-            .state
-            .core
-            .label_names
-            .get(&(self.state.core.funcs, self.label))
-        {
+        let key = (self.state.core.funcs, self.label);
+        let has_name = if let Some(name) = self.state.core.label_names.get(&key) {
+            self.printer.result.push_str(" ");
             name.write(&mut self.printer.result);
-            preceding_space = false;
             true
         } else {
             false
@@ -85,29 +78,23 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
         match ty {
             BlockType::Empty => {}
             BlockType::Type(t) => {
-                if !preceding_space {
-                    self.push_str(" ");
-                }
-                self.push_str("(result ");
+                self.push_str(" (result ");
                 self.printer.print_valtype(t)?;
                 self.push_str(")");
-                preceding_space = false;
             }
             BlockType::FuncType(idx) => {
-                if !preceding_space {
-                    self.push_str(" ");
-                }
+                self.push_str(" ");
                 self.printer
                     .print_core_functype_idx(self.state, idx, None)?;
-                preceding_space = false;
             }
         }
+        Ok(has_name)
+    }
 
+    fn maybe_blockty_label_comment(&mut self, has_name: bool) -> Result<()> {
         if !has_name {
             let depth = self.cur_depth();
-            if !preceding_space {
-                self.push_str(" ");
-            }
+            self.push_str(" ");
             // Note that 1 is added to the current depth here since if a block
             // type is being printed then a block is being created which will
             // increase the label depth of the block itself.
@@ -123,7 +110,7 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
     }
 
     fn tag_index(&mut self, index: u32) -> Result<()> {
-        write!(self.result(), "{index}")?;
+        self.printer.print_idx(&self.state.core.tag_names, index)?;
         Ok(())
     }
 
@@ -305,6 +292,48 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
         }
         Ok(())
     }
+
+    fn try_table(&mut self, table: TryTable) -> Result<()> {
+        let has_name = self.blockty_without_label_comment(table.ty)?;
+
+        // Nesting has already been incremented but labels for catch start above
+        // this `try_table` not at the `try_table`. Temporarily decrement this
+        // nesting count and increase it below after printing catch clauses.
+        self.printer.nesting -= 1;
+
+        for catch in table.catches {
+            self.result().push(' ');
+            match catch {
+                Catch::One { tag, label } => {
+                    self.printer.start_group("catch ");
+                    self.tag_index(tag)?;
+                    self.result().push(' ');
+                    self.relative_depth(label)?;
+                    self.printer.end_group();
+                }
+                Catch::OneRef { tag, label } => {
+                    self.printer.start_group("catch_ref ");
+                    self.tag_index(tag)?;
+                    self.result().push(' ');
+                    self.relative_depth(label)?;
+                    self.printer.end_group();
+                }
+                Catch::All { label } => {
+                    self.printer.start_group("catch_all ");
+                    self.relative_depth(label)?;
+                    self.printer.end_group();
+                }
+                Catch::AllRef { label } => {
+                    self.printer.start_group("catch_all_ref ");
+                    self.relative_depth(label)?;
+                    self.printer.end_group();
+                }
+            }
+        }
+        self.printer.nesting += 1;
+        self.maybe_blockty_label_comment(has_name)?;
+        Ok(())
+    }
 }
 
 #[derive(PartialEq, Copy, Clone)]
@@ -343,11 +372,12 @@ macro_rules! define_visit {
     (kind Loop) => (OpKind::BlockStart);
     (kind If) => (OpKind::BlockStart);
     (kind Try) => (OpKind::BlockStart);
-    (kind Else) => (OpKind::BlockMid);
+    (kind TryTable) => (OpKind::BlockStart);
     (kind Catch) => (OpKind::BlockMid);
     (kind CatchAll) => (OpKind::BlockMid);
-    (kind End) => (OpKind::End);
     (kind Delegate) => (OpKind::Delegate);
+    (kind Else) => (OpKind::BlockMid);
+    (kind End) => (OpKind::End);
     (kind $other:tt) => (OpKind::Normal);
 
     // How to print the payload of an instruction. There are a number of
@@ -1004,12 +1034,6 @@ macro_rules! define_visit {
     (name F64x2ConvertLowI32x4U) => ("f64x2.convert_low_i32x4_u");
     (name F32x4DemoteF64x2Zero) => ("f32x4.demote_f64x2_zero");
     (name F64x2PromoteLowF32x4) => ("f64x2.promote_low_f32x4");
-    (name Try) => ("try");
-    (name Catch) => ("catch");
-    (name Throw) => ("throw");
-    (name Rethrow) => ("rethrow");
-    (name Delegate) => ("delegate");
-    (name CatchAll) => ("catch_all");
     (name I8x16RelaxedSwizzle) => ("i8x16.relaxed_swizzle");
     (name I32x4RelaxedTruncF32x4S) => ("i32x4.relaxed_trunc_f32x4_s");
     (name I32x4RelaxedTruncF32x4U) => ("i32x4.relaxed_trunc_f32x4_u");
@@ -1061,6 +1085,14 @@ macro_rules! define_visit {
     (name RefI31) => ("ref.i31");
     (name I31GetS) => ("i31.get_s");
     (name I31GetU) => ("i31.get_u");
+    (name TryTable) => ("try_table");
+    (name Throw) => ("throw");
+    (name ThrowRef) => ("throw_ref");
+    (name Rethrow) => ("rethrow");
+    (name Try) => ("try");
+    (name Catch) => ("catch");
+    (name CatchAll) => ("catch_all");
+    (name Delegate) => ("delegate");
 }
 
 impl<'a> VisitOperator<'a> for PrintOperator<'_, '_> {
diff --git a/crates/wast/src/core/binary.rs b/crates/wast/src/core/binary.rs
index c45a0a59..68facd64 100644
--- a/crates/wast/src/core/binary.rs
+++ b/crates/wast/src/core/binary.rs
@@ -933,6 +933,7 @@ fn find_names<'a>(
                         | Instruction::Block(block)
                         | Instruction::Loop(block)
                         | Instruction::Try(block)
+                        | Instruction::TryTable(TryTable { block, .. })
                         | Instruction::Let(LetType { block, .. }) => {
                             if let Some(name) = get_name(&block.label, &block.label_name) {
                                 label_names.push((label_idx, name));
@@ -969,7 +970,8 @@ impl Names<'_> {
             && self.types.is_empty()
             && self.data.is_empty()
             && self.elems.is_empty()
-        // NB: specifically don't check tags/modules/instances since they're
+            && self.tags.is_empty()
+        // NB: specifically don't check modules/instances since they're
         // not encoded for now.
     }
 }
@@ -1024,6 +1026,10 @@ impl Encode for Names<'_> {
             self.data.encode(&mut tmp);
             subsec(9, &mut tmp);
         }
+        if self.tags.len() > 0 {
+            self.tags.encode(&mut tmp);
+            subsec(11, &mut tmp);
+        }
     }
 }
 
diff --git a/crates/wast/src/core/expr.rs b/crates/wast/src/core/expr.rs
index 78e2694e..489ac205 100644
--- a/crates/wast/src/core/expr.rs
+++ b/crates/wast/src/core/expr.rs
@@ -89,13 +89,6 @@ enum Level<'a> {
     /// which don't correspond to terminating instructions, we're just in a
     /// nested block.
     IfArm,
-
-    /// Similar to `If` but for `Try` statements, which has simpler parsing
-    /// state to track.
-    Try(Try<'a>),
-
-    /// Similar to `IfArm` but for `(do ...)` and `(catch ...)` blocks.
-    TryArm,
 }
 
 /// Possible states of "what is currently being parsed?" in an `if` expression.
@@ -112,21 +105,6 @@ enum If<'a> {
     Else,
 }
 
-/// Possible state of "what should be parsed next?" in a `try` expression.
-enum Try<'a> {
-    /// Next thing to parse is the `do` block.
-    Do(Instruction<'a>),
-    /// Next thing to parse is `catch`/`catch_all`, or `delegate`.
-    CatchOrDelegate,
-    /// Next thing to parse is a `catch` block or `catch_all`.
-    Catch,
-    /// Finished parsing like the `End` case, but does not push `end` opcode.
-    Delegate,
-    /// This `try` statement has finished parsing and if anything remains it's a
-    /// syntax error.
-    End,
-}
-
 impl<'a> ExpressionParser<'a> {
     fn parse(&mut self, parser: Parser<'a>) -> Result<()> {
         // Here we parse instructions in a loop, and we do not recursively
@@ -141,7 +119,7 @@ impl<'a> ExpressionParser<'a> {
             // As a small ease-of-life adjustment here, if we're parsing inside
             // of an `if block then we require that all sub-components are
             // s-expressions surrounded by `(` and `)`, so verify that here.
-            if let Some(Level::If(_)) | Some(Level::Try(_)) = self.stack.last() {
+            if let Some(Level::If(_)) = self.stack.last() {
                 if !parser.is_empty() && !parser.peek::<LParen>()? {
                     return Err(parser.error("expected `(`"));
                 }
@@ -167,18 +145,14 @@ impl<'a> ExpressionParser<'a> {
                     if self.handle_if_lparen(parser)? {
                         continue;
                     }
-                    // Second, we handle `try` parsing, which is simpler than
-                    // `if` but more complicated than, e.g., `block`.
-                    if self.handle_try_lparen(parser)? {
-                        continue;
-                    }
                     match parser.parse()? {
                         // If block/loop show up then we just need to be sure to
                         // push an `end` instruction whenever the `)` token is
                         // seen
                         i @ Instruction::Block(_)
                         | i @ Instruction::Loop(_)
-                        | i @ Instruction::Let(_) => {
+                        | i @ Instruction::Let(_)
+                        | i @ Instruction::TryTable(_) => {
                             self.instrs.push(i);
                             self.stack.push(Level::EndWith(Instruction::End(None)));
                         }
@@ -190,12 +164,6 @@ impl<'a> ExpressionParser<'a> {
                             self.stack.push(Level::If(If::Clause(i)));
                         }
 
-                        // Parsing a `try` is easier than `if` but we also push
-                        // a `Try` scope to handle the required nested blocks.
-                        i @ Instruction::Try(_) => {
-                            self.stack.push(Level::Try(Try::Do(i)));
-                        }
-
                         // Anything else means that we're parsing a nested form
                         // such as `(i32.add ...)` which means that the
                         // instruction we parsed will be coming at the end.
@@ -209,7 +177,6 @@ impl<'a> ExpressionParser<'a> {
                 Paren::Right => match self.stack.pop().unwrap() {
                     Level::EndWith(i) => self.instrs.push(i),
                     Level::IfArm => {}
-                    Level::TryArm => {}
 
                     // If an `if` statement hasn't parsed the clause or `then`
                     // block, then that's an error because there weren't enough
@@ -221,17 +188,6 @@ impl<'a> ExpressionParser<'a> {
                     Level::If(_) => {
                         self.instrs.push(Instruction::End(None));
                     }
-
-                    // The `do` clause is required in a `try` statement, so
-                    // we will signal that error here. Otherwise, terminate with
-                    // an `end` or `delegate` instruction.
-                    Level::Try(Try::Do(_)) => {
-                        return Err(parser.error("previous `try` had no `do`"));
-                    }
-                    Level::Try(Try::Delegate) => {}
-                    Level::Try(_) => {
-                        self.instrs.push(Instruction::End(None));
-                    }
                 },
             }
         }
@@ -331,93 +287,6 @@ impl<'a> ExpressionParser<'a> {
             If::Else => Err(parser.error("unexpected token: too many payloads inside of `(if)`")),
         }
     }
-
-    /// Handles parsing of a `try` statement. A `try` statement is simpler
-    /// than an `if` as the syntactic form is:
-    ///
-    /// ```wat
-    /// (try (do $do) (catch $tag $catch))
-    /// ```
-    ///
-    /// where the `do` and `catch` keywords are mandatory, even for an empty
-    /// $do or $catch.
-    ///
-    /// Returns `true` if the rest of the arm above should be skipped, or
-    /// `false` if we should parse the next item as an instruction (because we
-    /// didn't handle the lparen here).
-    fn handle_try_lparen(&mut self, parser: Parser<'a>) -> Result<bool> {
-        // Only execute the code below if there's a `Try` listed last.
-        let i = match self.stack.last_mut() {
-            Some(Level::Try(i)) => i,
-            _ => return Ok(false),
-        };
-
-        // Try statements must start with a `do` block.
-        if let Try::Do(try_instr) = i {
-            let instr = mem::replace(try_instr, Instruction::End(None));
-            self.instrs.push(instr);
-            if parser.parse::<Option<kw::r#do>>()?.is_some() {
-                // The state is advanced here only if the parse succeeds in
-                // order to strictly require the keyword.
-                *i = Try::CatchOrDelegate;
-                self.stack.push(Level::TryArm);
-                return Ok(true);
-            }
-            // We return here and continue parsing instead of raising an error
-            // immediately because the missing keyword will be caught more
-            // generally in the `Paren::Right` case in `parse`.
-            return Ok(false);
-        }
-
-        // After a try's `do`, there are several possible kinds of handlers.
-        if let Try::CatchOrDelegate = i {
-            // `catch` may be followed by more `catch`s or `catch_all`.
-            if parser.parse::<Option<kw::catch>>()?.is_some() {
-                let evt = parser.parse::<Index<'a>>()?;
-                self.instrs.push(Instruction::Catch(evt));
-                *i = Try::Catch;
-                self.stack.push(Level::TryArm);
-                return Ok(true);
-            }
-            // `catch_all` can only come at the end and has no argument.
-            if parser.parse::<Option<kw::catch_all>>()?.is_some() {
-                self.instrs.push(Instruction::CatchAll);
-                *i = Try::End;
-                self.stack.push(Level::TryArm);
-                return Ok(true);
-            }
-            // `delegate` has an index, and also ends the block like `end`.
-            if parser.parse::<Option<kw::delegate>>()?.is_some() {
-                let depth = parser.parse::<Index<'a>>()?;
-                self.instrs.push(Instruction::Delegate(depth));
-                *i = Try::Delegate;
-                match self.paren(parser)? {
-                    Paren::Left | Paren::None => return Ok(false),
-                    Paren::Right => return Ok(true),
-                }
-            }
-            return Err(parser.error("expected a `catch`, `catch_all`, or `delegate`"));
-        }
-
-        if let Try::Catch = i {
-            if parser.parse::<Option<kw::catch>>()?.is_some() {
-                let evt = parser.parse::<Index<'a>>()?;
-                self.instrs.push(Instruction::Catch(evt));
-                *i = Try::Catch;
-                self.stack.push(Level::TryArm);
-                return Ok(true);
-            }
-            if parser.parse::<Option<kw::catch_all>>()?.is_some() {
-                self.instrs.push(Instruction::CatchAll);
-                *i = Try::End;
-                self.stack.push(Level::TryArm);
-                return Ok(true);
-            }
-            return Err(parser.error("unexpected items after `catch`"));
-        }
-
-        Err(parser.error("unexpected token: too many payloads inside of `(try)`"))
-    }
 }
 
 // TODO: document this obscenity
@@ -1142,17 +1011,17 @@ instructions! {
         F64x2PromoteLowF32x4 : [0xfd, 95] : "f64x2.promote_low_f32x4",
 
         // Exception handling proposal
+        ThrowRef : [0x0a] : "throw_ref",
+        TryTable(TryTable<'a>) : [0x1f] : "try_table",
+        Throw(Index<'a>) : [0x08] : "throw",
+
+        // Deprecated exception handling optocdes
         Try(Box<BlockType<'a>>) : [0x06] : "try",
         Catch(Index<'a>) : [0x07] : "catch",
-        Throw(Index<'a>) : [0x08] : "throw",
         Rethrow(Index<'a>) : [0x09] : "rethrow",
         Delegate(Index<'a>) : [0x18] : "delegate",
         CatchAll : [0x19] : "catch_all",
 
-        // Exception handling proposal extension for 'exnref'
-        ThrowRef : [0x0a] : "throw_ref",
-        TryTable(TryTable<'a>) : [0x1f] : "try_table",
-
         // Relaxed SIMD proposal
         I8x16RelaxedSwizzle : [0xfd, 0x100]: "i8x16.relaxed_swizzle",
         I32x4RelaxedTruncF32x4S : [0xfd, 0x101]: "i32x4.relaxed_trunc_f32x4_s",
@@ -1235,10 +1104,11 @@ impl<'a> Parse<'a> for TryTable<'a> {
         let block = parser.parse()?;
 
         let mut catches = Vec::new();
-        while parser.peek2::<kw::catch>()?
-            || parser.peek2::<kw::catch_ref>()?
-            || parser.peek2::<kw::catch_all>()?
-            || parser.peek2::<kw::catch_all_ref>()?
+        while parser.peek::<LParen>()?
+            && (parser.peek2::<kw::catch>()?
+                || parser.peek2::<kw::catch_ref>()?
+                || parser.peek2::<kw::catch_all>()?
+                || parser.peek2::<kw::catch_all_ref>()?)
         {
             catches.push(parser.parens(|p| {
                 let kind = if parser.peek::<kw::catch_ref>()? {
diff --git a/crates/wast/src/core/resolve/names.rs b/crates/wast/src/core/resolve/names.rs
index acd84e17..05894e9a 100644
--- a/crates/wast/src/core/resolve/names.rs
+++ b/crates/wast/src/core/resolve/names.rs
@@ -522,10 +522,6 @@ impl<'a, 'b> ExprResolver<'a, 'b> {
                 self.resolve_block_type(bt)?;
             }
             TryTable(try_table) => {
-                self.blocks.push(ExprBlock {
-                    label: try_table.block.label,
-                    pushed_scope: false,
-                });
                 self.resolve_block_type(&mut try_table.block)?;
                 for catch in &mut try_table.catches {
                     if let Some(tag) = catch.kind.tag_index_mut() {
@@ -533,6 +529,10 @@ impl<'a, 'b> ExprResolver<'a, 'b> {
                     }
                     self.resolve_label(&mut catch.label)?;
                 }
+                self.blocks.push(ExprBlock {
+                    label: try_table.block.label,
+                    pushed_scope: false,
+                });
             }
 
             // On `End` instructions we pop a label from the stack, and for both
@@ -580,15 +580,14 @@ impl<'a, 'b> ExprResolver<'a, 'b> {
                 self.resolve_label(&mut i.default)?;
             }
 
-            Throw(i) => {
+            Throw(i) | Catch(i) => {
                 self.resolver.resolve(i, Ns::Tag)?;
             }
+
             Rethrow(i) => {
                 self.resolve_label(i)?;
             }
-            Catch(i) => {
-                self.resolver.resolve(i, Ns::Tag)?;
-            }
+
             Delegate(i) => {
                 // Since a delegate starts counting one layer out from the
                 // current try-delegate block, we pop before we resolve labels.
diff --git a/crates/wit-component/src/gc.rs b/crates/wit-component/src/gc.rs
index 9ebdaf66..a672335c 100644
--- a/crates/wit-component/src/gc.rs
+++ b/crates/wit-component/src/gc.rs
@@ -495,7 +495,8 @@ impl<'a> Module<'a> {
             | HeapType::Eq
             | HeapType::Struct
             | HeapType::Array
-            | HeapType::I31 => {}
+            | HeapType::I31
+            | HeapType::Exn => {}
             HeapType::Concrete(i) => self.ty(i.as_module_index().unwrap()),
         }
     }
@@ -1042,6 +1043,7 @@ macro_rules! define_visit {
     (mark_live $self:ident $arg:ident field_index) => {};
     (mark_live $self:ident $arg:ident from_type_nullable) => {};
     (mark_live $self:ident $arg:ident to_type_nullable) => {};
+    (mark_live $self:ident $arg:ident try_table) => {unimplemented!();};
 }
 
 impl<'a> VisitOperator<'a> for Module<'a> {
@@ -1130,6 +1132,7 @@ impl Encoder {
             HeapType::Struct => wasm_encoder::HeapType::Struct,
             HeapType::Array => wasm_encoder::HeapType::Array,
             HeapType::I31 => wasm_encoder::HeapType::I31,
+            HeapType::Exn => wasm_encoder::HeapType::Exn,
             HeapType::Concrete(idx) => {
                 wasm_encoder::HeapType::Concrete(self.types.remap(idx.as_module_index().unwrap()))
             }
@@ -1186,6 +1189,10 @@ macro_rules! define_encode {
         let _ = $mem_byte;
         MemoryGrow($mem)
     });
+    (mk TryTable $try_table:ident) => ({
+        let _ = $try_table;
+        unimplemented_try_table()
+    });
     (mk I32Const $v:ident) => (I32Const($v));
     (mk I64Const $v:ident) => (I64Const($v));
     (mk F32Const $v:ident) => (F32Const(f32::from_bits($v.bits())));
@@ -1238,12 +1245,17 @@ macro_rules! define_encode {
     (map $self:ident $arg:ident field_index) => {$arg};
     (map $self:ident $arg:ident from_type_nullable) => {$arg};
     (map $self:ident $arg:ident to_type_nullable) => {$arg};
+    (map $self:ident $arg:ident try_table) => {$arg};
     (map $self:ident $arg:ident targets) => ((
         $arg.targets().map(|i| i.unwrap()).collect::<Vec<_>>().into(),
         $arg.default(),
     ));
 }
 
+fn unimplemented_try_table() -> wasm_encoder::Instruction<'static> {
+    unimplemented!()
+}
+
 impl<'a> VisitOperator<'a> for Encoder {
     type Output = ();
 
diff --git a/fuzz/src/roundtrip.rs b/fuzz/src/roundtrip.rs
index 70c6daf7..221193bb 100644
--- a/fuzz/src/roundtrip.rs
+++ b/fuzz/src/roundtrip.rs
@@ -76,7 +76,8 @@ fn validate_name_section(wasm: &[u8]) -> wasmparser::Result<()> {
                 | Name::Memory(n)
                 | Name::Global(n)
                 | Name::Element(n)
-                | Name::Data(n) => {
+                | Name::Data(n)
+                | Name::Tag(n) => {
                     for name in n {
                         name?;
                     }
diff --git a/src/bin/wasm-tools/demangle.rs b/src/bin/wasm-tools/demangle.rs
index 262ec312..297e299c 100644
--- a/src/bin/wasm-tools/demangle.rs
+++ b/src/bin/wasm-tools/demangle.rs
@@ -73,6 +73,7 @@ impl Opts {
                 Name::Data(names) => new_section.data(&self.name_map(names)?),
                 Name::Local(names) => new_section.locals(&self.indirect_name_map(names)?),
                 Name::Label(names) => new_section.labels(&self.indirect_name_map(names)?),
+                Name::Tag(names) => new_section.tags(&self.name_map(names)?),
                 Name::Unknown { .. } => bail!("unknown name section"),
             }
         }
diff --git a/src/bin/wasm-tools/dump.rs b/src/bin/wasm-tools/dump.rs
index 01175a75..92230561 100644
--- a/src/bin/wasm-tools/dump.rs
+++ b/src/bin/wasm-tools/dump.rs
@@ -550,6 +550,7 @@ impl<'a> Dump<'a> {
             Name::Global(n) => self.print_name_map("global", n)?,
             Name::Element(n) => self.print_name_map("element", n)?,
             Name::Data(n) => self.print_name_map("data", n)?,
+            Name::Tag(n) => self.print_name_map("tag", n)?,
             Name::Unknown { ty, range, .. } => {
                 write!(self.state, "unknown names: {}", ty)?;
                 self.print(range.start)?;


Test patch:
diff --git a/crates/wit-component/tests/components/link-initialize/component.wat b/crates/wit-component/tests/components/link-initialize/component.wat
index d801299f..f00ef17a 100644
--- a/crates/wit-component/tests/components/link-initialize/component.wat
+++ b/crates/wit-component/tests/components/link-initialize/component.wat
@@ -69,7 +69,7 @@
       (mem-info (memory 4 4))
       (needed "c")
     )
-    (type $.data (;0;) (func))
+    (type (;0;) (func))
     (type (;1;) (func (param i32) (result i32)))
     (import "env" "memory" (memory (;0;) 1))
     (import "env" "__indirect_function_table" (table (;0;) 0 funcref))
@@ -77,11 +77,11 @@
     (import "env" "__memory_base" (global $__memory_base (;1;) i32))
     (import "env" "__table_base" (global $__table_base (;2;) i32))
     (import "env" "malloc" (func $malloc (;0;) (type 1)))
-    (import "env" "abort" (func $abort (;1;) (type $.data)))
+    (import "env" "abort" (func $abort (;1;) (type 0)))
     (import "GOT.mem" "um" (global $um (;3;) (mut i32)))
     (import "test:test/test" "bar" (func $bar (;2;) (type 1)))
-    (func $_initialize (;3;) (type $.data))
-    (func $__wasm_apply_data_relocs (;4;) (type $.data))
+    (func $_initialize (;3;) (type 0))
+    (func $__wasm_apply_data_relocs (;4;) (type 0))
     (func $foo (;5;) (type 1) (param i32) (result i32)
       global.get $__stack_pointer
       i32.const 16
@@ -112,24 +112,24 @@
     (export "__wasm_apply_data_relocs" (func $__wasm_apply_data_relocs))
     (export "foo" (func $foo))
     (export "well" (global 4))
-    (data (;0;) (global.get $__memory_base) "\04\00\00\00")
+    (data $.data (;0;) (global.get $__memory_base) "\04\00\00\00")
   )
   (core module (;3;)
     (@dylink.0
       (mem-info (memory 20 4))
       (needed "foo")
     )
-    (type $.data (;0;) (func (param i32) (result i32)))
+    (type (;0;) (func (param i32) (result i32)))
     (type (;1;) (func))
     (import "env" "memory" (memory (;0;) 1))
     (import "env" "__indirect_function_table" (table (;0;) 0 funcref))
     (import "env" "__memory_base" (global $__memory_base (;0;) i32))
     (import "env" "__table_base" (global $__table_base (;1;) i32))
-    (import "env" "foo" (func $foo (;0;) (type $.data)))
+    (import "env" "foo" (func $foo (;0;) (type 0)))
     (import "GOT.mem" "well" (global $well (;2;) (mut i32)))
     (func $_initialize (;1;) (type 1))
     (func $__wasm_apply_data_relocs (;2;) (type 1))
-    (func $bar (;3;) (type $.data) (param i32) (result i32)
+    (func $bar (;3;) (type 0) (param i32) (result i32)
       local.get 0
       call $foo
       global.get $well
@@ -141,7 +141,7 @@
     (export "__wasm_apply_data_relocs" (func $__wasm_apply_data_relocs))
     (export "test:test/test#bar" (func $bar))
     (export "um" (global 3))
-    (data (;0;) (global.get $__memory_base) "\01\00\00\00\02\00\00\00\03\00\00\00\04\00\00\00\05\00\00\00")
+    (data $.data (;0;) (global.get $__memory_base) "\01\00\00\00\02\00\00\00\03\00\00\00\04\00\00\00\05\00\00\00")
   )
   (core module (;4;)
     (type (;0;) (func))
diff --git a/crates/wit-component/tests/components/link/component.wat b/crates/wit-component/tests/components/link/component.wat
index d78029e7..7ada35a5 100644
--- a/crates/wit-component/tests/components/link/component.wat
+++ b/crates/wit-component/tests/components/link/component.wat
@@ -69,7 +69,7 @@
       (mem-info (memory 4 4))
       (needed "c")
     )
-    (type $.data (;0;) (func))
+    (type (;0;) (func))
     (type (;1;) (func (param i32) (result i32)))
     (import "env" "memory" (memory (;0;) 1))
     (import "env" "__indirect_function_table" (table (;0;) 0 funcref))
@@ -77,11 +77,11 @@
     (import "env" "__memory_base" (global $__memory_base (;1;) i32))
     (import "env" "__table_base" (global $__table_base (;2;) i32))
     (import "env" "malloc" (func $malloc (;0;) (type 1)))
-    (import "env" "abort" (func $abort (;1;) (type $.data)))
+    (import "env" "abort" (func $abort (;1;) (type 0)))
     (import "GOT.mem" "um" (global $um (;3;) (mut i32)))
     (import "test:test/test" "bar" (func $bar (;2;) (type 1)))
-    (func $__wasm_call_ctors (;3;) (type $.data))
-    (func $__wasm_apply_data_relocs (;4;) (type $.data))
+    (func $__wasm_call_ctors (;3;) (type 0))
+    (func $__wasm_apply_data_relocs (;4;) (type 0))
     (func $foo (;5;) (type 1) (param i32) (result i32)
       global.get $__stack_pointer
       i32.const 16
@@ -112,24 +112,24 @@
     (export "__wasm_apply_data_relocs" (func $__wasm_apply_data_relocs))
     (export "foo" (func $foo))
     (export "well" (global 4))
-    (data (;0;) (global.get $__memory_base) "\04\00\00\00")
+    (data $.data (;0;) (global.get $__memory_base) "\04\00\00\00")
   )
   (core module (;3;)
     (@dylink.0
       (mem-info (memory 20 4))
       (needed "foo")
     )
-    (type $.data (;0;) (func (param i32) (result i32)))
+    (type (;0;) (func (param i32) (result i32)))
     (type (;1;) (func))
     (import "env" "memory" (memory (;0;) 1))
     (import "env" "__indirect_function_table" (table (;0;) 0 funcref))
     (import "env" "__memory_base" (global $__memory_base (;0;) i32))
     (import "env" "__table_base" (global $__table_base (;1;) i32))
-    (import "env" "foo" (func $foo (;0;) (type $.data)))
+    (import "env" "foo" (func $foo (;0;) (type 0)))
     (import "GOT.mem" "well" (global $well (;2;) (mut i32)))
     (func $__wasm_call_ctors (;1;) (type 1))
     (func $__wasm_apply_data_relocs (;2;) (type 1))
-    (func $bar (;3;) (type $.data) (param i32) (result i32)
+    (func $bar (;3;) (type 0) (param i32) (result i32)
       local.get 0
       call $foo
       global.get $well
@@ -141,7 +141,7 @@
     (export "__wasm_apply_data_relocs" (func $__wasm_apply_data_relocs))
     (export "test:test/test#bar" (func $bar))
     (export "um" (global 3))
-    (data (;0;) (global.get $__memory_base) "\01\00\00\00\02\00\00\00\03\00\00\00\04\00\00\00\05\00\00\00")
+    (data $.data (;0;) (global.get $__memory_base) "\01\00\00\00\02\00\00\00\03\00\00\00\04\00\00\00\05\00\00\00")
   )
   (core module (;4;)
     (type (;0;) (func))
diff --git a/tests/cli/dump/try-delegate.wat b/tests/cli/dump/try-delegate.wat
deleted file mode 100644
index 85738baf..00000000
--- a/tests/cli/dump/try-delegate.wat
+++ /dev/null
@@ -1,10 +0,0 @@
-;; RUN: dump %
-
-(module
-  (func
-    try $l
-      try
-      delegate $l
-    end
-  )
-)
diff --git a/tests/cli/dump/try-delegate.wat.stdout b/tests/cli/dump/try-delegate.wat.stdout
deleted file mode 100644
index 5503e0d4..00000000
--- a/tests/cli/dump/try-delegate.wat.stdout
+++ /dev/null
@@ -1,26 +0,0 @@
-  0x0 | 00 61 73 6d | version 1 (Module)
-      | 01 00 00 00
-  0x8 | 01 04       | type section
-  0xa | 01          | 1 count
-  0xb | 60 00 00    | [type 0] RecGroup { inner: Implicit(SubType { is_final: true, supertype_idx: None, composite_type: Func(FuncType { params: [], results: [] }) }) }
-  0xe | 03 02       | func section
- 0x10 | 01          | 1 count
- 0x11 | 00          | [func 0] type 0
- 0x12 | 0a 0b       | code section
- 0x14 | 01          | 1 count
-============== func 0 ====================
- 0x15 | 09          | size of function
- 0x16 | 00          | 0 local blocks
- 0x17 | 06 40       | try blockty:Empty
- 0x19 | 06 40       | try blockty:Empty
- 0x1b | 18 00       | delegate relative_depth:0
- 0x1d | 0b          | end
- 0x1e | 0b          | end
- 0x1f | 00 0d       | custom section
- 0x21 | 04 6e 61 6d | name: "name"
-      | 65         
- 0x26 | 03 06       | label section
- 0x28 | 01          | 1 count
- 0x29 | 00          | function 0 label name section
- 0x2a | 01          | 1 count
- 0x2b | 00 01 6c    | Naming { index: 0, name: "l" }
diff --git a/tests/local/exception-handling.wast b/tests/cli/print-deprecated-exceptions.wat
similarity index 59%
rename from tests/local/exception-handling.wast
rename to tests/cli/print-deprecated-exceptions.wat
index 092a2dfe..2b942e2c 100644
--- a/tests/local/exception-handling.wast
+++ b/tests/cli/print-deprecated-exceptions.wat
@@ -1,4 +1,5 @@
-;; --enable-exceptions --enable-multi-value
+;; RUN: print %
+
 (module
   (type (func (param i32 i64)))
   (type (func (param i32)))
@@ -39,25 +40,11 @@
     drop
     drop
   )
+  (func $mix-old-and-new
+    try_table
+      try
+      catch_all
+      end
+    end
+  )
 )
-
-(assert_invalid
-  (module
-    (type (func))
-    (func throw 0))
-  "unknown tag 0: tag index out of bounds")
-
-(assert_invalid
-  (module
-    (func try catch_all catch_all end))
-  "only one catch_all allowed per `try` block")
-
-(assert_invalid
-  (module
-    (func try catch_all catch 0 end))
-  "catch found outside of an `try` block")
-
-(assert_invalid
-  (module
-    (func block try catch_all rethrow 1 end end))
-  "target was not a `catch` block")
diff --git a/tests/snapshots/local/exception-handling.wast/0.print b/tests/cli/print-deprecated-exceptions.wat.stdout
similarity index 87%
rename from tests/snapshots/local/exception-handling.wast/0.print
rename to tests/cli/print-deprecated-exceptions.wat.stdout
index 52d4eb2d..f3cb45ed 100644
--- a/tests/snapshots/local/exception-handling.wast/0.print
+++ b/tests/cli/print-deprecated-exceptions.wat.stdout
@@ -39,5 +39,12 @@
     drop
     drop
   )
+  (func $mix-old-and-new (;3;) (type 2)
+    try_table ;; label = @1
+      try ;; label = @2
+      catch_all
+      end
+    end
+  )
   (tag (;1;) (type 1) (param i32))
 )
\ No newline at end of file
diff --git a/tests/cli/print-deprecated-exceptions2.wat b/tests/cli/print-deprecated-exceptions2.wat
new file mode 100644
index 00000000..c2dba240
--- /dev/null
+++ b/tests/cli/print-deprecated-exceptions2.wat
@@ -0,0 +1,47 @@
+;; RUN: print %
+
+(module $m
+  (tag)
+  (func
+    try
+    end
+  )
+  (func
+    try
+    catch 0
+    end
+  )
+  (func
+    try
+    catch 0
+      rethrow 0
+    end
+  )
+  (func
+    try
+    catch_all
+      rethrow 0
+    end
+  )
+  (func
+    try
+    catch 0
+    catch_all
+      rethrow 0
+    end
+  )
+  (func
+    try
+      try
+      delegate 0
+    catch 0
+    end
+  )
+  (func (result i32)
+    try (result i32)
+      i32.const 42
+    catch 0
+      i32.const 42
+    end
+  )
+)
diff --git a/tests/snapshots/local/try.wat.print b/tests/cli/print-deprecated-exceptions2.wat.stdout
similarity index 100%
rename from tests/snapshots/local/try.wat.print
rename to tests/cli/print-deprecated-exceptions2.wat.stdout
diff --git a/tests/local/exnref/try_table.wast b/tests/local/exnref/try_table.wast
deleted file mode 100644
index ade6a0a5..00000000
--- a/tests/local/exnref/try_table.wast
+++ /dev/null
@@ -1,95 +0,0 @@
-(module
-  (tag $a (param i32))
-
-  (func
-    (; empty try_table ;)
-    try_table
-    end
-
-    (; try_table with result  ;)
-    try_table (result i32)
-      i32.const 0
-    end
-    drop
-
-    (; try_table can catches ;)
-    try_table (catch $a 0)
-    end
-
-    try_table (catch $a 0) (catch $a 0)
-    end
-
-    try_table (catch_all 0)
-    end
-
-    try_table (catch $a 0) (catch_all 0)
-    end
-
-    try_table (catch $a 0) (catch $a 0) (catch_all 0)
-    end
-
-    (; try_table can have results and catches ;)
-    try_table (result i32) (catch $a 0)
-      i32.const 0
-    end
-    drop
-
-    try_table (result i32) (catch $a 0) (catch $a 0)
-      i32.const 0
-    end
-    drop
-
-    try_table (result i32) (catch_all 0)
-      i32.const 0
-    end
-    drop
-
-    try_table (result i32) (catch $a 0) (catch_all 0)
-      i32.const 0
-    end
-    drop
-
-    try_table (result i32) (catch $a 0) (catch $a 0) (catch_all 0)
-      i32.const 0
-    end
-    drop
-
-    (; mixes of catch, catch_ref, catch_all, and catch_all_ref ;)
-
-    try_table (catch_ref $a 0)
-    end
-
-    try_table (catch_ref $a 0) (catch_ref $a 0)
-    end
-
-    try_table (catch $a 0) (catch_ref $a 0)
-    end
-
-    try_table (catch_ref $a 0) (catch_ref $a 0)
-    end
-
-    try_table (catch_ref $a 0) (catch $a 0)
-    end
-
-    try_table (catch_all 0)
-    end
-
-    try_table (catch_all_ref 0)
-    end
-
-    try_table (catch_ref $a 0) (catch_all_ref 0)
-    end
-
-    try_table (catch_ref $a 0) (catch_ref $a 0) (catch_all_ref 0)
-    end
-
-    try_table (catch $a 0) (catch_ref $a 0) (catch_all_ref 0)
-    end
-
-    try_table (catch_ref $a 0) (catch_ref $a 0) (catch_all_ref 0)
-    end
-
-    try_table (catch_ref $a 0) (catch $a 0) (catch_all_ref 0)
-    end
-  )
-)
diff --git a/tests/local/try.wast b/tests/local/try.wast
deleted file mode 100644
index e4ffa23d..00000000
--- a/tests/local/try.wast
+++ /dev/null
@@ -1,55 +0,0 @@
-;; --enable-exceptions
-
-(assert_malformed
-  (module quote
-    "(func (try))"
-  )
-  "previous `try` had no `do`")
-
-(assert_malformed
-  (module quote
-    "(func (try (catch $exn)))"
-  )
-  "previous `try` had no `do`")
-
-(assert_malformed
-  (module quote
-    "(func (try (unreachable) (catch $exn)))"
-  )
-  "previous `try` had no `do`")
-
-(assert_malformed
-  (module quote
-    "(func (try (do) (unreachable)))"
-  )
-  "expected a `catch`, `catch_all`, or `delegate`")
-
-(assert_malformed
-  (module quote
-    "(func (try (do) (catch_all) (unreachable)))"
-  )
-  "too many payloads inside of `(try)`")
-
-(assert_malformed
-  (module quote
-    "(func (try (do) (catch $exn) drop))"
-  )
-  "expected `(`")
-
-(assert_malformed
-  (module quote
-    "(func (try (do) (catch $exn) (drop)))"
-  )
-  "unexpected items after `catch`")
-
-(assert_malformed
-  (module quote
-    "(func (try (do) (delegate 0) (drop)))"
-  )
-  "too many payloads inside of `(try)`")
-
-(assert_malformed
-  (module quote
-    "(func (try $l (do) (delegate $l)))"
-  )
-  "unknown label")
diff --git a/tests/local/try.wat b/tests/local/try.wat
deleted file mode 100644
index c91f1ad6..00000000
--- a/tests/local/try.wat
+++ /dev/null
@@ -1,15 +0,0 @@
-;; --enable-exceptions
-
-(module $m
-  (type (func))
-  (tag $exn (type 0))
-  (func (try (do)))
-  (func (try (do) (catch $exn)))
-  (func (try (do) (catch $exn rethrow 0)))
-  (func (try (do) (catch_all rethrow 0)))
-  (func (try (do) (catch $exn) (catch_all rethrow 0)))
-  (func (try (do (try (do) (delegate 0))) (catch $exn)))
-  (func (result i32)
-    (try (result i32)
-      (do (i32.const 42))
-      (catch $exn (i32.const 42)))))
diff --git a/tests/roundtrip.rs b/tests/roundtrip.rs
index e2a964ab..ced12df2 100644
--- a/tests/roundtrip.rs
+++ b/tests/roundtrip.rs
@@ -141,22 +141,7 @@ fn skip_test(test: &Path, contents: &[u8]) -> bool {
     false
 }
 
-fn skip_validation(test: &Path) -> bool {
-    let broken = &[
-        "exnref/exnref.wast",
-        "exnref/throw_ref.wast",
-        "exnref/try_table.wast",
-        "exception-handling/ref_null.wast",
-        "exception-handling/throw.wast",
-        "exception-handling/throw_ref.wast",
-        "exception-handling/try_catch.wast",
-        "exception-handling/try_delegate.wast",
-        "exception-handling/try_table.wast",
-    ];
-    if broken.iter().any(|x| test.ends_with(x)) {
-        return true;
-    }
-
+fn skip_validation(_test: &Path) -> bool {
     false
 }
 
@@ -670,7 +655,8 @@ fn error_matches(error: &str, message: &str) -> bool {
     {
         return error.contains("expected ")
             || error.contains("constant out of range")
-            || error.contains("extra tokens remaining");
+            || error.contains("extra tokens remaining")
+            || error.contains("unimplemented validation of deprecated opcode");
     }
 
     if message == "illegal character" {
@@ -818,5 +804,9 @@ fn error_matches(error: &str, message: &str) -> bool {
         return error.starts_with("unknown operator") || error.starts_with("unexpected token");
     }
 
+    if message.starts_with("type mismatch") {
+        return error.starts_with("type mismatch");
+    }
+
     return false;
 }
diff --git a/tests/snapshots/local/exnref/exnref.wast/0.print b/tests/snapshots/local/exnref/exnref.wast/0.print
new file mode 100644
index 00000000..a8e59be1
--- /dev/null
+++ b/tests/snapshots/local/exnref/exnref.wast/0.print
@@ -0,0 +1,7 @@
+(module
+  (type (;0;) (func (param exnref)))
+  (type (;1;) (func (param (ref exn))))
+  (func (;0;) (type 0) (param exnref))
+  (func (;1;) (type 0) (param exnref))
+  (func (;2;) (type 1) (param (ref exn)))
+)
\ No newline at end of file
diff --git a/tests/snapshots/local/exnref/throw_ref.wast/0.print b/tests/snapshots/local/exnref/throw_ref.wast/0.print
new file mode 100644
index 00000000..631e1528
--- /dev/null
+++ b/tests/snapshots/local/exnref/throw_ref.wast/0.print
@@ -0,0 +1,7 @@
+(module
+  (type (;0;) (func (param exnref)))
+  (func (;0;) (type 0) (param exnref)
+    local.get 0
+    throw_ref
+  )
+)
\ No newline at end of file
diff --git a/tests/snapshots/testsuite/proposals/exception-handling/imports.wast/0.print b/tests/snapshots/testsuite/proposals/exception-handling/imports.wast/0.print
index b149a05a..7a7038a3 100644
--- a/tests/snapshots/testsuite/proposals/exception-handling/imports.wast/0.print
+++ b/tests/snapshots/testsuite/proposals/exception-handling/imports.wast/0.print
@@ -25,7 +25,7 @@
   (table (;1;) 10 20 funcref)
   (memory (;0;) 2)
   (tag (;0;) (type 0))
-  (tag (;1;) (type 1) (param i32))
+  (tag $tag-i32 (;1;) (type 1) (param i32))
   (tag (;2;) (type 2) (param f32))
   (global (;0;) i32 i32.const 55)
   (global (;1;) f32 f32.const 0x1.6p+5 (;=44;))
diff --git a/tests/snapshots/testsuite/proposals/exception-handling/ref_null.wast/0.print b/tests/snapshots/testsuite/proposals/exception-handling/ref_null.wast/0.print
new file mode 100644
index 00000000..20e88ebd
--- /dev/null
+++ b/tests/snapshots/testsuite/proposals/exception-handling/ref_null.wast/0.print
@@ -0,0 +1,20 @@
+(module
+  (type (;0;) (func (result externref)))
+  (type (;1;) (func (result exnref)))
+  (type (;2;) (func (result funcref)))
+  (func (;0;) (type 0) (result externref)
+    ref.null extern
+  )
+  (func (;1;) (type 1) (result exnref)
+    ref.null exn
+  )
+  (func (;2;) (type 2) (result funcref)
+    ref.null func
+  )
+  (global (;0;) externref ref.null extern)
+  (global (;1;) exnref ref.null exn)
+  (global (;2;) funcref ref.null func)
+  (export "externref" (func 0))
+  (export "exnref" (func 1))
+  (export "funcref" (func 2))
+)
\ No newline at end of file
diff --git a/tests/snapshots/testsuite/proposals/exception-handling/tag.wast/0.print b/tests/snapshots/testsuite/proposals/exception-handling/tag.wast/0.print
index 36049d8d..26e3a246 100644
--- a/tests/snapshots/testsuite/proposals/exception-handling/tag.wast/0.print
+++ b/tests/snapshots/testsuite/proposals/exception-handling/tag.wast/0.print
@@ -5,7 +5,7 @@
   (tag (;0;) (type 0))
   (tag (;1;) (type 1) (param i32))
   (tag (;2;) (type 1) (param i32))
-  (tag (;3;) (type 2) (param i32 f32))
+  (tag $t3 (;3;) (type 2) (param i32 f32))
   (export "t2" (tag 2))
   (export "t3" (tag 3))
 )
\ No newline at end of file
diff --git a/tests/snapshots/testsuite/proposals/exception-handling/tag.wast/2.print b/tests/snapshots/testsuite/proposals/exception-handling/tag.wast/2.print
index d9f5a1d5..039051e3 100644
--- a/tests/snapshots/testsuite/proposals/exception-handling/tag.wast/2.print
+++ b/tests/snapshots/testsuite/proposals/exception-handling/tag.wast/2.print
@@ -1,6 +1,6 @@
 (module
   (type (;0;) (func (param i32)))
   (type (;1;) (func (param i32 f32)))
-  (import "test" "t2" (tag (;0;) (type 0) (param i32)))
-  (import "test" "t3" (tag (;1;) (type 1) (param i32 f32)))
+  (import "test" "t2" (tag $t0 (;0;) (type 0) (param i32)))
+  (import "test" "t3" (tag $t1 (;1;) (type 1) (param i32 f32)))
 )
\ No newline at end of file
diff --git a/tests/snapshots/testsuite/proposals/exception-handling/throw.wast/0.print b/tests/snapshots/testsuite/proposals/exception-handling/throw.wast/0.print
new file mode 100644
index 00000000..144f1456
--- /dev/null
+++ b/tests/snapshots/testsuite/proposals/exception-handling/throw.wast/0.print
@@ -0,0 +1,77 @@
+(module
+  (type (;0;) (func))
+  (type (;1;) (func (param i32)))
+  (type (;2;) (func (param f32)))
+  (type (;3;) (func (param i64)))
+  (type (;4;) (func (param f64)))
+  (type (;5;) (func (param i32 i32)))
+  (type (;6;) (func (param i32) (result i32)))
+  (type (;7;) (func (result i32 i32)))
+  (func $throw-if (;0;) (type 6) (param i32) (result i32)
+    local.get 0
+    i32.const 0
+    i32.ne
+    if ;; label = @1
+      throw $e0
+    end
+    i32.const 0
+  )
+  (func (;1;) (type 2) (param f32)
+    local.get 0
+    throw $e-f32
+  )
+  (func (;2;) (type 3) (param i64)
+    local.get 0
+    throw $e-i64
+  )
+  (func (;3;) (type 4) (param f64)
+    local.get 0
+    throw $e-f64
+  )
+  (func (;4;) (type 0)
+    throw $e0
+    throw $e-i32
+  )
+  (func (;5;) (type 0)
+    block (result i32) ;; label = @1
+      throw $e0
+    end
+    throw $e-i32
+  )
+  (func $throw-1-2 (;6;) (type 0)
+    i32.const 1
+    i32.const 2
+    throw $e-i32-i32
+  )
+  (func (;7;) (type 0)
+    block $h (type 7) (result i32 i32)
+      try_table (catch $e-i32-i32 $h) ;; label = @2
+        call $throw-1-2
+      end
+      return
+    end
+    i32.const 2
+    i32.ne
+    if ;; label = @1
+      unreachable
+    end
+    i32.const 1
+    i32.ne
+    if ;; label = @1
+      unreachable
+    end
+  )
+  (tag $e0 (;0;) (type 0))
+  (tag $e-i32 (;1;) (type 1) (param i32))
+  (tag $e-f32 (;2;) (type 2) (param f32))
+  (tag $e-i64 (;3;) (type 3) (param i64))
+  (tag $e-f64 (;4;) (type 4) (param f64))
+  (tag $e-i32-i32 (;5;) (type 5) (param i32 i32))
+  (export "throw-if" (func $throw-if))
+  (export "throw-param-f32" (func 1))
+  (export "throw-param-i64" (func 2))
+  (export "throw-param-f64" (func 3))
+  (export "throw-polymorphic" (func 4))
+  (export "throw-polymorphic-block" (func 5))
+  (export "test-throw-1-2" (func 7))
+)
\ No newline at end of file
diff --git a/tests/snapshots/testsuite/proposals/exception-handling/throw_ref.wast/0.print b/tests/snapshots/testsuite/proposals/exception-handling/throw_ref.wast/0.print
new file mode 100644
index 00000000..60b430aa
--- /dev/null
+++ b/tests/snapshots/testsuite/proposals/exception-handling/throw_ref.wast/0.print
@@ -0,0 +1,132 @@
+(module
+  (type (;0;) (func))
+  (type (;1;) (func (param i32) (result i32)))
+  (type (;2;) (func (param exnref)))
+  (func (;0;) (type 0)
+    block $h (result exnref)
+      try_table (catch_ref $e0 $h) ;; label = @2
+        throw $e0
+      end
+      unreachable
+    end
+    throw_ref
+  )
+  (func (;1;) (type 1) (param i32) (result i32)
+    block $h (result exnref)
+      try_table (result i32) (catch_ref $e0 $h) ;; label = @2
+        throw $e0
+      end
+      return
+    end
+    local.get 0
+    i32.eqz
+    if (type 2) (param exnref) ;; label = @1
+      throw_ref
+    else
+      drop
+    end
+    i32.const 23
+  )
+  (func (;2;) (type 0)
+    block $h (result exnref)
+      try_table (result exnref) (catch_all_ref $h) ;; label = @2
+        throw $e0
+      end
+    end
+    throw_ref
+  )
+  (func (;3;) (type 1) (param i32) (result i32)
+    block $h (result exnref)
+      try_table (result i32) (catch_all_ref $h) ;; label = @2
+        throw $e0
+      end
+      return
+    end
+    local.get 0
+    i32.eqz
+    if (type 2) (param exnref) ;; label = @1
+      throw_ref
+    else
+      drop
+    end
+    i32.const 23
+  )
+  (func (;4;) (type 1) (param i32) (result i32)
+    (local $exn1 exnref) (local $exn2 exnref)
+    block $h1 (result exnref)
+      try_table (result i32) (catch_ref $e1 $h1) ;; label = @2
+        throw $e1
+      end
+      return
+    end
+    local.set $exn1
+    block $h2 (result exnref)
+      try_table (result i32) (catch_ref $e0 $h2) ;; label = @2
+        throw $e0
+      end
+      return
+    end
+    local.set $exn2
+    local.get 0
+    i32.const 0
+    i32.eq
+    if ;; label = @1
+      local.get $exn1
+      throw_ref
+    end
+    local.get 0
+    i32.const 1
+    i32.eq
+    if ;; label = @1
+      local.get $exn2
+      throw_ref
+    end
+    i32.const 23
+  )
+  (func (;5;) (type 1) (param i32) (result i32)
+    (local $e exnref)
+    block $h1 (result exnref)
+      try_table (result i32) (catch_ref $e0 $h1) ;; label = @2
+        throw $e0
+      end
+      return
+    end
+    local.set $e
+    block $h2 (result exnref)
+      try_table (result i32) (catch_ref $e0 $h2) ;; label = @2
+        local.get 0
+        i32.eqz
+        if ;; label = @3
+          local.get $e
+          throw_ref
+        end
+        i32.const 42
+      end
+      return
+    end
+    drop
+    i32.const 23
+  )
+  (func (;6;) (type 0)
+    (local $e exnref)
+    block $h (result exnref)
+      try_table (result f64) (catch_ref $e0 $h) ;; label = @2
+        throw $e0
+      end
+      unreachable
+    end
+    local.set $e
+    i32.const 1
+    local.get $e
+    throw_ref
+  )
+  (tag $e0 (;0;) (type 0))
+  (tag $e1 (;1;) (type 0))
+  (export "catch-throw_ref-0" (func 0))
+  (export "catch-throw_ref-1" (func 1))
+  (export "catchall-throw_ref-0" (func 2))
+  (export "catchall-throw_ref-1" (func 3))
+  (export "throw_ref-nested" (func 4))
+  (export "throw_ref-recatch" (func 5))
+  (export "throw_ref-stack-polymorphism" (func 6))
+)
\ No newline at end of file
diff --git a/tests/snapshots/testsuite/proposals/exception-handling/try_table.wast/0.print b/tests/snapshots/testsuite/proposals/exception-handling/try_table.wast/0.print
new file mode 100644
index 00000000..139d36d2
--- /dev/null
+++ b/tests/snapshots/testsuite/proposals/exception-handling/try_table.wast/0.print
@@ -0,0 +1,9 @@
+(module
+  (type (;0;) (func))
+  (func (;0;) (type 0)
+    throw $e0
+  )
+  (tag $e0 (;0;) (type 0))
+  (export "e0" (tag 0))
+  (export "throw" (func 0))
+)
\ No newline at end of file
diff --git a/tests/snapshots/testsuite/proposals/exception-handling/try_table.wast/2.print b/tests/snapshots/testsuite/proposals/exception-handling/try_table.wast/2.print
new file mode 100644
index 00000000..7dcb8da4
--- /dev/null
+++ b/tests/snapshots/testsuite/proposals/exception-handling/try_table.wast/2.print
@@ -0,0 +1,296 @@
+(module
+  (type (;0;) (func))
+  (type (;1;) (func (param i32)))
+  (type (;2;) (func (param f32)))
+  (type (;3;) (func (param i64)))
+  (type (;4;) (func (param f64)))
+  (type (;5;) (func (param i32) (result i32)))
+  (type (;6;) (func (param i32 i32) (result i32)))
+  (type (;7;) (func (param f32) (result f32)))
+  (type (;8;) (func (param i64) (result i64)))
+  (type (;9;) (func (param f64) (result f64)))
+  (type (;10;) (func (result i32 exnref)))
+  (type (;11;) (func (result f32 exnref)))
+  (type (;12;) (func (result i64 exnref)))
+  (type (;13;) (func (result f64 exnref)))
+  (type (;14;) (func (result i32)))
+  (import "test" "e0" (tag $imported-e0 (;0;) (type 0)))
+  (import "test" "throw" (func $imported-throw (;0;) (type 0)))
+  (func $throw-if (;1;) (type 5) (param i32) (result i32)
+    local.get 0
+    i32.const 0
+    i32.ne
+    if ;; label = @1
+      throw $e0
+    end
+    i32.const 0
+  )
+  (func (;2;) (type 5) (param i32) (result i32)
+    block $h
+      try_table (result i32) (catch $e0 $h) ;; label = @2
+        local.get 0
+        i32.eqz
+        if ;; label = @3
+          throw $e0
+        else
+        end
+        i32.const 42
+      end
+      return
+    end
+    i32.const 23
+  )
+  (func (;3;) (type 0)
+    block $h
+      try_table (catch_all $h) ;; label = @2
+        unreachable
+      end
+      return
+    end
+  )
+  (func $div (;4;) (type 6) (param i32 i32) (result i32)
+    local.get 0
+    local.get 1
+    i32.div_u
+  )
+  (func (;5;) (type 6) (param i32 i32) (result i32)
+    block $h
+      try_table (result i32) (catch_all $h) ;; label = @2
+        local.get 0
+        local.get 1
+        call $div
+      end
+      return
+    end
+    i32.const 11
+  )
+  (func (;6;) (type 5) (param i32) (result i32)
+    block $h1
+      try_table (result i32) (catch $e1 $h1) ;; label = @2
+        block $h0
+          try_table (result i32) (catch $e0 $h0) ;; label = @4
+            local.get 0
+            i32.eqz
+            if ;; label = @5
+              throw $e0
+            else
+              local.get 0
+              i32.const 1
+              i32.eq
+              if ;; label = @6
+                throw $e1
+              else
+                throw $e2
+              end
+            end
+            i32.const 2
+          end
+          br 1 (;@2;)
+        end
+        i32.const 3
+      end
+      return
+    end
+    i32.const 4
+  )
+  (func (;7;) (type 5) (param i32) (result i32)
+    block $h0
+      block $h1
+        try_table (result i32) (catch $e0 $h0) (catch $e1 $h1) ;; label = @3
+          local.get 0
+          i32.eqz
+          if ;; label = @4
+            throw $e0
+          else
+            local.get 0
+            i32.const 1
+            i32.eq
+            if ;; label = @5
+              throw $e1
+            else
+              throw $e2
+            end
+          end
+          i32.const 2
+        end
+        return
+      end
+      i32.const 4
+      return
+    end
+    i32.const 3
+  )
+  (func (;8;) (type 5) (param i32) (result i32)
+    block $h (result i32)
+      try_table (result i32) (catch $e-i32 $h) ;; label = @2
+        local.get 0
+        throw $e-i32
+        i32.const 2
+      end
+      return
+    end
+    return
+  )
+  (func (;9;) (type 7) (param f32) (result f32)
+    block $h (result f32)
+      try_table (result f32) (catch $e-f32 $h) ;; label = @2
+        local.get 0
+        throw $e-f32
+        f32.const 0x0p+0 (;=0;)
+      end
+      return
+    end
+    return
+  )
+  (func (;10;) (type 8) (param i64) (result i64)
+    block $h (result i64)
+      try_table (result i64) (catch $e-i64 $h) ;; label = @2
+        local.get 0
+        throw $e-i64
+        i64.const 2
+      end
+      return
+    end
+    return
+  )
+  (func (;11;) (type 9) (param f64) (result f64)
+    block $h (result f64)
+      try_table (result f64) (catch $e-f64 $h) ;; label = @2
+        local.get 0
+        throw $e-f64
+        f64.const 0x0p+0 (;=0;)
+      end
+      return
+    end
+    return
+  )
+  (func (;12;) (type 5) (param i32) (result i32)
+    block $h (type 10) (result i32 exnref)
+      try_table (result i32) (catch_ref $e-i32 $h) ;; label = @2
+        local.get 0
+        throw $e-i32
+        i32.const 2
+      end
+      return
+    end
+    drop
+    return
+  )
+  (func (;13;) (type 7) (param f32) (result f32)
+    block $h (type 11) (result f32 exnref)
+      try_table (result f32) (catch_ref $e-f32 $h) ;; label = @2
+        local.get 0
+        throw $e-f32
+        f32.const 0x0p+0 (;=0;)
+      end
+      return
+    end
+    drop
+    return
+  )
+  (func (;14;) (type 8) (param i64) (result i64)
+    block $h (type 12) (result i64 exnref)
+      try_table (result i64) (catch_ref $e-i64 $h) ;; label = @2
+        local.get 0
+        throw $e-i64
+        i64.const 2
+      end
+      return
+    end
+    drop
+    return
+  )
+  (func (;15;) (type 9) (param f64) (result f64)
+    block $h (type 13) (result f64 exnref)
+      try_table (result f64) (catch_ref $e-f64 $h) ;; label = @2
+        local.get 0
+        throw $e-f64
+        f64.const 0x0p+0 (;=0;)
+      end
+      return
+    end
+    drop
+    return
+  )
+  (func $throw-param-i32 (;16;) (type 1) (param i32)
+    local.get 0
+    throw $e-i32
+  )
+  (func (;17;) (type 5) (param i32) (result i32)
+    block $h (result i32)
+      try_table (result i32) (catch $e-i32 $h) ;; label = @2
+        i32.const 0
+        local.get 0
+        call $throw-param-i32
+      end
+      return
+    end
+  )
+  (func (;18;) (type 14) (result i32)
+    block $h
+      try_table (result i32) (catch $imported-e0 $h) ;; label = @2
+        i32.const 1
+        call $imported-throw
+      end
+      return
+    end
+    i32.const 2
+  )
+  (func (;19;) (type 5) (param i32) (result i32)
+    block $h
+      try_table (result i32) (catch $e0 $h) ;; label = @2
+        try_table (result i32) ;; label = @3
+          local.get 0
+          call $throw-if
+        end
+      end
+      return
+    end
+    i32.const 1
+  )
+  (func $throw-void (;20;) (type 0)
+    throw $e0
+  )
+  (func (;21;) (type 0)
+    block $h
+      try_table (catch $e0 $h) ;; label = @2
+        return_call $throw-void
+      end
+    end
+  )
+  (func (;22;) (type 0)
+    block $h
+      try_table (catch $e0 $h) ;; label = @2
+        i32.const 0
+        return_call_indirect (type 0)
+      end
+    end
+  )
+  (table (;0;) 1 1 funcref)
+  (tag $e0 (;1;) (type 0))
+  (tag $e1 (;2;) (type 0))
+  (tag $e2 (;3;) (type 0))
+  (tag $e-i32 (;4;) (type 1) (param i32))
+  (tag $e-f32 (;5;) (type 2) (param f32))
+  (tag $e-i64 (;6;) (type 3) (param i64))
+  (tag $e-f64 (;7;) (type 4) (param f64))
+  (export "simple-throw-catch" (func 2))
+  (export "unreachable-not-caught" (func 3))
+  (export "trap-in-callee" (func 5))
+  (export "catch-complex-1" (func 6))
+  (export "catch-complex-2" (func 7))
+  (export "throw-catch-param-i32" (func 8))
+  (export "throw-catch-param-f32" (func 9))
+  (export "throw-catch-param-i64" (func 10))
+  (export "throw-catch-param-f64" (func 11))
+  (export "throw-catch_ref-param-i32" (func 12))
+  (export "throw-catch_ref-param-f32" (func 13))
+  (export "throw-catch_ref-param-i64" (func 14))
+  (export "throw-catch_ref-param-f64" (func 15))
+  (export "catch-param-i32" (func 17))
+  (export "catch-imported" (func 18))
+  (export "catchless-try" (func 19))
+  (export "return-call-in-try-catch" (func 21))
+  (export "return-call-indirect-in-try-catch" (func 22))
+  (elem (;0;) (i32.const 0) func $throw-void)
+)
\ No newline at end of file
diff --git a/tests/snapshots/testsuite/proposals/exception-handling/try_table.wast/40.print b/tests/snapshots/testsuite/proposals/exception-handling/try_table.wast/40.print
new file mode 100644
index 00000000..f818e2ed
--- /dev/null
+++ b/tests/snapshots/testsuite/proposals/exception-handling/try_table.wast/40.print
@@ -0,0 +1,23 @@
+(module
+  (type (;0;) (func))
+  (type (;1;) (func (result i32)))
+  (import "test" "throw" (func $imported-throw (;0;) (type 0)))
+  (func (;1;) (type 1) (result i32)
+    block $h
+      try_table (result i32) (catch_all $h) ;; label = @2
+        block $h0
+          try_table (result i32) (catch $e0 $h0) ;; label = @4
+            i32.const 1
+            call $imported-throw
+          end
+          return
+        end
+        i32.const 2
+      end
+      return
+    end
+    i32.const 3
+  )
+  (tag $e0 (;0;) (type 0))
+  (export "imported-mismatch" (func 1))
+)
\ No newline at end of file
diff --git a/tests/snapshots/testsuite/proposals/exception-handling/try_table.wast/44.print b/tests/snapshots/testsuite/proposals/exception-handling/try_table.wast/44.print
new file mode 100644
index 00000000..59c03b27
--- /dev/null
+++ b/tests/snapshots/testsuite/proposals/exception-handling/try_table.wast/44.print
@@ -0,0 +1,32 @@
+(module
+  (type (;0;) (func))
+  (type (;1;) (func (result exnref)))
+  (func (;0;) (type 0)
+    try_table (catch $e 0 (;@0;)) (catch $e 0 (;@0;)) ;; label = @1
+    end
+  )
+  (func (;1;) (type 0)
+    try_table (catch_all 0 (;@0;)) (catch $e 0 (;@0;)) ;; label = @1
+    end
+  )
+  (func (;2;) (type 0)
+    try_table (catch_all 0 (;@0;)) (catch_all 0 (;@0;)) ;; label = @1
+    end
+  )
+  (func (;3;) (type 1) (result exnref)
+    try_table (catch_ref $e 0 (;@0;)) (catch_ref $e 0 (;@0;)) ;; label = @1
+    end
+    unreachable
+  )
+  (func (;4;) (type 1) (result exnref)
+    try_table (catch_all_ref 0 (;@0;)) (catch_ref $e 0 (;@0;)) ;; label = @1
+    end
+    unreachable
+  )
+  (func (;5;) (type 1) (result exnref)
+    try_table (catch_all_ref 0 (;@0;)) (catch_all_ref 0 (;@0;)) ;; label = @1
+    end
+    unreachable
+  )
+  (tag $e (;0;) (type 0))
+)
\ No newline at end of file