Repository: denoland/deno_core
Pull request #536 (2024-02-01 17:20:39)

chore(core): port Deno resource tests and re-organize error test code

 - Moves the resource test from Deno to cover #533 
 - Moves some of the error ops around to centralize them



Patch:
diff --git a/core/error.rs b/core/error.rs
index bc4530d..7b8376f 100644
--- a/core/error.rs
+++ b/core/error.rs
@@ -11,8 +11,6 @@ use anyhow::Error;
 
 use crate::runtime::JsRealm;
 use crate::runtime::JsRuntime;
-use crate::source_map::apply_source_map;
-use crate::source_map::get_source_line;
 use crate::source_map::SourceMapApplication;
 use crate::url::Url;
 
@@ -213,21 +211,8 @@ impl JsStackFrame {
     // V8's column numbers are 0-based, we want 1-based.
     let c = message.get_start_column() as u32 + 1;
     let state = JsRuntime::state_from(scope);
-    let (getter, cache) = {
-      (
-        state.source_map_getter.clone(),
-        state.source_map_cache.clone(),
-      )
-    };
-
-    let application = if let Some(source_map_getter) = getter {
-      let mut cache = cache.borrow_mut();
-      apply_source_map(&f, l, c, &mut cache, &**source_map_getter)
-    } else {
-      SourceMapApplication::Unchanged
-    };
-
-    match application {
+    let mut source_mapper = state.source_mapper.borrow_mut();
+    match source_mapper.apply_source_map(&f, l, c) {
       SourceMapApplication::Unchanged => Some(JsStackFrame::from_location(
         Some(f),
         Some(l.into()),
@@ -331,28 +316,15 @@ impl JsError {
     }
     {
       let state = JsRuntime::state_from(scope);
-      let (getter, cache) = {
-        (
-          state.source_map_getter.clone(),
-          state.source_map_cache.clone(),
-        )
-      };
-      if let Some(source_map_getter) = getter {
-        let mut cache = cache.borrow_mut();
-        for (i, frame) in frames.iter().enumerate() {
-          if let (Some(file_name), Some(line_number)) =
-            (&frame.file_name, frame.line_number)
-          {
-            if !file_name.trim_start_matches('[').starts_with("ext:") {
-              source_line = get_source_line(
-                file_name,
-                line_number,
-                &mut cache,
-                &**source_map_getter,
-              );
-              source_line_frame_index = Some(i);
-              break;
-            }
+      let mut source_mapper = state.source_mapper.borrow_mut();
+      for (i, frame) in frames.iter().enumerate() {
+        if let (Some(file_name), Some(line_number)) =
+          (&frame.file_name, frame.line_number)
+        {
+          if !file_name.trim_start_matches('[').starts_with("ext:") {
+            source_line = source_mapper.get_source_line(file_name, line_number);
+            source_line_frame_index = Some(i);
+            break;
           }
         }
       }
@@ -462,26 +434,15 @@ impl JsError {
       }
       {
         let state = JsRuntime::state_from(scope);
-        let (getter, cache) = {
-          (
-            state.source_map_getter.clone(),
-            state.source_map_cache.clone(),
-          )
-        };
-        if let Some(source_map_getter) = getter {
-          let mut cache = cache.borrow_mut();
-
+        let mut source_mapper = state.source_mapper.borrow_mut();
+        if source_mapper.has_user_sources() {
           for (i, frame) in frames.iter().enumerate() {
             if let (Some(file_name), Some(line_number)) =
               (&frame.file_name, frame.line_number)
             {
               if !file_name.trim_start_matches('[').starts_with("ext:") {
-                source_line = get_source_line(
-                  file_name,
-                  line_number,
-                  &mut cache,
-                  &**source_map_getter,
-                );
+                source_line =
+                  source_mapper.get_source_line(file_name, line_number);
                 source_line_frame_index = Some(i);
                 break;
               }
diff --git a/core/lib.deno_core.d.ts b/core/lib.deno_core.d.ts
index 45e60e0..e75dd66 100644
--- a/core/lib.deno_core.d.ts
+++ b/core/lib.deno_core.d.ts
@@ -195,6 +195,11 @@ declare namespace Deno {
 
     export type UncaughtExceptionCallback = (err: any) => void;
 
+    export class BadResource extends Error {}
+    export const BadResourcePrototype: typeof BadResource.prototype;
+    export class Interrupted extends Error {}
+    export const InterruptedPrototype: typeof Interrupted.prototype;
+
     function serialize(
       value: any,
       options?: any,
diff --git a/core/ops_builtin_v8.rs b/core/ops_builtin_v8.rs
index 117b121..7965929 100644
--- a/core/ops_builtin_v8.rs
+++ b/core/ops_builtin_v8.rs
@@ -10,7 +10,6 @@ use crate::resolve_url;
 use crate::runtime::script_origin;
 use crate::runtime::JsRealm;
 use crate::runtime::JsRuntimeState;
-use crate::source_map::apply_source_map;
 use crate::source_map::SourceMapApplication;
 use crate::JsBuffer;
 use crate::JsRuntime;
@@ -896,36 +895,27 @@ pub fn op_apply_source_map(
   if ret_buf.len() != 8 {
     return Err(type_error("retBuf must be 8 bytes"));
   }
-  if let Some(source_map_getter) = state.source_map_getter.as_ref() {
-    let mut cache = state.source_map_cache.borrow_mut();
-    let application = apply_source_map(
-      file_name,
+  let mut source_mapper = state.source_mapper.borrow_mut();
+  let application =
+    source_mapper.apply_source_map(file_name, line_number, column_number);
+  match application {
+    SourceMapApplication::Unchanged => Ok(0),
+    SourceMapApplication::LineAndColumn {
       line_number,
       column_number,
-      &mut cache,
-      &***source_map_getter,
-    );
-    match application {
-      SourceMapApplication::Unchanged => Ok(0),
-      SourceMapApplication::LineAndColumn {
-        line_number,
-        column_number,
-      } => {
-        write_line_and_col_to_ret_buf(ret_buf, line_number, column_number);
-        Ok(1)
-      }
-      SourceMapApplication::LineAndColumnAndFileName {
-        line_number,
-        column_number,
-        file_name,
-      } => {
-        write_line_and_col_to_ret_buf(ret_buf, line_number, column_number);
-        cache.stashed_file_name.replace(file_name);
-        Ok(2)
-      }
+    } => {
+      write_line_and_col_to_ret_buf(ret_buf, line_number, column_number);
+      Ok(1)
+    }
+    SourceMapApplication::LineAndColumnAndFileName {
+      line_number,
+      column_number,
+      file_name,
+    } => {
+      write_line_and_col_to_ret_buf(ret_buf, line_number, column_number);
+      source_mapper.stashed_file_name.replace(file_name);
+      Ok(2)
     }
-  } else {
-    Ok(0)
   }
 }
 
@@ -937,7 +927,7 @@ pub fn op_apply_source_map_filename(
   state: &JsRuntimeState,
 ) -> Result<String, Error> {
   state
-    .source_map_cache
+    .source_mapper
     .borrow_mut()
     .stashed_file_name
     .take()
@@ -970,20 +960,10 @@ pub fn op_current_user_call_site(
     }
     let line_number = frame.get_line_number() as u32;
     let column_number = frame.get_column() as u32;
-    let application = if let Some(source_map_getter) =
-      js_runtime_state.source_map_getter.as_ref()
-    {
-      let mut cache = js_runtime_state.source_map_cache.borrow_mut();
-      apply_source_map(
-        &file_name,
-        line_number,
-        column_number,
-        &mut cache,
-        &***source_map_getter,
-      )
-    } else {
-      SourceMapApplication::Unchanged
-    };
+    let application = js_runtime_state
+      .source_mapper
+      .borrow_mut()
+      .apply_source_map(&file_name, line_number, column_number);
 
     match application {
       SourceMapApplication::Unchanged => {
diff --git a/core/runtime/bindings.rs b/core/runtime/bindings.rs
index 4477399..ddd910f 100644
--- a/core/runtime/bindings.rs
+++ b/core/runtime/bindings.rs
@@ -116,7 +116,7 @@ pub fn script_origin<'a>(
 pub(crate) fn get<'s, T>(
   scope: &mut v8::HandleScope<'s>,
   from: v8::Local<v8::Object>,
-  key: &'static [u8],
+  key: &'static v8::OneByteConst,
   path: &'static str,
 ) -> T
 where
@@ -132,32 +132,49 @@ where
 
 pub mod v8_static_strings {
   pub fn new<'s>(
+    scope: &mut v8::HandleScope<'s>,
+    str_: &'static v8::OneByteConst,
+  ) -> v8::Local<'s, v8::String> {
+    v8::String::new_from_onebyte_const(scope, str_).unwrap()
+  }
+
+  pub fn new_from_static_str<'s>(
     scope: &mut v8::HandleScope<'s>,
     str_: &'static [u8],
   ) -> v8::Local<'s, v8::String> {
     v8::String::new_external_onebyte_static(scope, str_).unwrap()
   }
 
-  pub static DENO: &[u8] = b"Deno";
-  pub static CORE: &[u8] = b"core";
-  pub static OPS: &[u8] = b"ops";
-  pub static URL: &[u8] = b"url";
-  pub static MAIN: &[u8] = b"main";
-  pub static RESOLVE: &[u8] = b"resolve";
-  pub static MESSAGE: &[u8] = b"message";
-  pub static CODE: &[u8] = b"code";
-  pub static ERR_MODULE_NOT_FOUND: &[u8] = b"ERR_MODULE_NOT_FOUND";
-  pub static EVENT_LOOP_TICK: &[u8] = b"eventLoopTick";
-  pub static BUILD_CUSTOM_ERROR: &[u8] = b"buildCustomError";
-  pub static CONSOLE: &[u8] = b"console";
-  pub static CALL_CONSOLE: &[u8] = b"callConsole";
-  pub static FILENAME: &[u8] = b"filename";
-  pub static DIRNAME: &[u8] = b"dirname";
-  pub static SET_UP_ASYNC_STUB: &[u8] = b"setUpAsyncStub";
-  pub static WEBASSEMBLY: &[u8] = b"WebAssembly";
-  pub static MODULE: &[u8] = b"Module";
-  pub static IMPORTS: &[u8] = b"imports";
-  pub static EXPORTS: &[u8] = b"exports";
+  macro_rules! onebyte_const {
+    ($str_:literal) => {
+      v8::String::create_external_onebyte_const($str_.as_bytes())
+    };
+  }
+
+  pub static DENO: v8::OneByteConst = onebyte_const!("Deno");
+  pub static CORE: v8::OneByteConst = onebyte_const!("core");
+  pub static OPS: v8::OneByteConst = onebyte_const!("ops");
+  pub static URL: v8::OneByteConst = onebyte_const!("url");
+  pub static MAIN: v8::OneByteConst = onebyte_const!("main");
+  pub static RESOLVE: v8::OneByteConst = onebyte_const!("resolve");
+  pub static MESSAGE: v8::OneByteConst = onebyte_const!("message");
+  pub static CODE: v8::OneByteConst = onebyte_const!("code");
+  pub static ERR_MODULE_NOT_FOUND: v8::OneByteConst =
+    onebyte_const!("ERR_MODULE_NOT_FOUND");
+  pub static EVENT_LOOP_TICK: v8::OneByteConst =
+    onebyte_const!("eventLoopTick");
+  pub static BUILD_CUSTOM_ERROR: v8::OneByteConst =
+    onebyte_const!("buildCustomError");
+  pub static CONSOLE: v8::OneByteConst = onebyte_const!("console");
+  pub static CALL_CONSOLE: v8::OneByteConst = onebyte_const!("callConsole");
+  pub static FILENAME: v8::OneByteConst = onebyte_const!("filename");
+  pub static DIRNAME: v8::OneByteConst = onebyte_const!("dirname");
+  pub static SET_UP_ASYNC_STUB: v8::OneByteConst =
+    onebyte_const!("setUpAsyncStub");
+  pub static WEBASSEMBLY: v8::OneByteConst = onebyte_const!("WebAssembly");
+  pub static MODULE: v8::OneByteConst = onebyte_const!("Module");
+  pub static IMPORTS: v8::OneByteConst = onebyte_const!("imports");
+  pub static EXPORTS: v8::OneByteConst = onebyte_const!("exports");
 }
 
 /// Create an object on the `globalThis` that looks like this:
@@ -178,7 +195,7 @@ pub(crate) fn initialize_deno_core_namespace<'s>(
   init_mode: InitMode,
 ) {
   let global = context.global(scope);
-  let deno_str = v8_static_strings::new(scope, v8_static_strings::DENO);
+  let deno_str = v8_static_strings::new(scope, &v8_static_strings::DENO);
 
   let maybe_deno_obj_val = global.get(scope, deno_str.into());
 
@@ -190,10 +207,11 @@ pub(crate) fn initialize_deno_core_namespace<'s>(
   }
 
   let deno_obj = v8::Object::new(scope);
-  let deno_core_key = v8_static_strings::new(scope, v8_static_strings::CORE);
+  let deno_core_key = v8_static_strings::new(scope, &v8_static_strings::CORE);
   // Set up `Deno.core.ops` object
   let deno_core_ops_obj = v8::Object::new(scope);
-  let deno_core_ops_key = v8_static_strings::new(scope, v8_static_strings::OPS);
+  let deno_core_ops_key =
+    v8_static_strings::new(scope, &v8_static_strings::OPS);
 
   let deno_core_obj = v8::Object::new(scope);
   deno_core_obj
@@ -205,7 +223,7 @@ pub(crate) fn initialize_deno_core_namespace<'s>(
     // Bind `call_console` to Deno.core.callConsole
     let call_console_fn = v8::Function::new(scope, call_console).unwrap();
     let call_console_key =
-      v8_static_strings::new(scope, v8_static_strings::CALL_CONSOLE);
+      v8_static_strings::new(scope, &v8_static_strings::CALL_CONSOLE);
     deno_core_obj.set(scope, call_console_key.into(), call_console_fn.into());
 
     // Bind v8 console object to Deno.core.console
@@ -213,10 +231,11 @@ pub(crate) fn initialize_deno_core_namespace<'s>(
     let console_obj: v8::Local<v8::Object> = get(
       scope,
       extra_binding_obj,
-      v8_static_strings::CONSOLE,
+      &v8_static_strings::CONSOLE,
       "ExtrasBindingObject.console",
     );
-    let console_key = v8_static_strings::new(scope, v8_static_strings::CONSOLE);
+    let console_key =
+      v8_static_strings::new(scope, &v8_static_strings::CONSOLE);
     deno_core_obj.set(scope, console_key.into(), console_obj.into());
   }
 
@@ -232,7 +251,10 @@ pub(crate) fn initialize_primordials_and_infra(
   for file_source in CONTEXT_SETUP_SOURCES {
     let code = file_source.load().unwrap();
     let source_str = code.v8_string(scope).unwrap();
-    let name = v8_static_strings::new(scope, file_source.specifier.as_bytes());
+    let name = v8_static_strings::new_from_static_str(
+      scope,
+      file_source.specifier.as_bytes(),
+    );
     let origin = script_origin(scope, name);
     // TODO(bartlomieju): these two calls will panic if there's any problem in the JS code
     let script = v8::Script::compile(scope, source_str, Some(&origin))
@@ -255,26 +277,29 @@ pub(crate) fn initialize_deno_core_ops_bindings<'s>(
   // Set up JavaScript bindings for the defined op - this will insert proper
   // `v8::Function` into `Deno.core.ops` object. For async ops, there a bit
   // more machinery involved, see comment below.
-  let deno_obj = get(scope, global, v8_static_strings::DENO, "Deno");
+  let deno_obj = get(scope, global, &v8_static_strings::DENO, "Deno");
   let deno_core_obj =
-    get(scope, deno_obj, v8_static_strings::CORE, "Deno.core");
+    get(scope, deno_obj, &v8_static_strings::CORE, "Deno.core");
   let deno_core_ops_obj: v8::Local<v8::Object> = get(
     scope,
     deno_core_obj,
-    v8_static_strings::OPS,
+    &v8_static_strings::OPS,
     "Deno.core.ops",
   );
   let set_up_async_stub_fn: v8::Local<v8::Function> = get(
     scope,
     deno_core_obj,
-    v8_static_strings::SET_UP_ASYNC_STUB,
+    &v8_static_strings::SET_UP_ASYNC_STUB,
     "Deno.core.setUpAsyncStub",
   );
 
   let undefined = v8::undefined(scope);
   for op_ctx in op_ctxs {
     let mut op_fn = op_ctx_function(scope, op_ctx);
-    let key = v8_static_strings::new(scope, op_ctx.decl.name.as_bytes());
+    let key = v8_static_strings::new_from_static_str(
+      scope,
+      op_ctx.decl.name.as_bytes(),
+    );
 
     // For async ops we need to set them up, by calling `Deno.core.setUpAsyncStub` -
     // this call will generate an optimized function that binds to the provided
@@ -296,7 +321,8 @@ fn op_ctx_function<'s>(
 ) -> v8::Local<'s, v8::Function> {
   let op_ctx_ptr = op_ctx as *const OpCtx as *const c_void;
   let external = v8::External::new(scope, op_ctx_ptr as *mut c_void);
-  let v8name = v8_static_strings::new(scope, op_ctx.decl.name.as_bytes());
+  let v8name =
+    v8_static_strings::new_from_static_str(scope, op_ctx.decl.name.as_bytes());
 
   let (slow_fn, fast_fn) = if op_ctx.metrics_enabled() {
     (
@@ -436,11 +462,11 @@ pub extern "C" fn host_initialize_import_meta_object_callback(
     .get_name_by_module(&module_global)
     .expect("Module not found");
 
-  let url_key = v8_static_strings::new(scope, v8_static_strings::URL);
+  let url_key = v8_static_strings::new(scope, &v8_static_strings::URL);
   let url_val = v8::String::new(scope, &name).unwrap();
   meta.create_data_property(scope, url_key.into(), url_val.into());
 
-  let main_key = v8_static_strings::new(scope, v8_static_strings::MAIN);
+  let main_key = v8_static_strings::new(scope, &v8_static_strings::MAIN);
   let main = module_map.is_main_module(&module_global);
   let main_val = v8::Boolean::new(scope, main);
   meta.create_data_property(scope, main_key.into(), main_val.into());
@@ -448,7 +474,7 @@ pub extern "C" fn host_initialize_import_meta_object_callback(
   let builder =
     v8::FunctionBuilder::new(import_meta_resolve).data(url_val.into());
   let val = v8::FunctionBuilder::<v8::Function>::build(builder, scope).unwrap();
-  let resolve_key = v8_static_strings::new(scope, v8_static_strings::RESOLVE);
+  let resolve_key = v8_static_strings::new(scope, &v8_static_strings::RESOLVE);
   meta.set(scope, resolve_key.into(), val.into());
 
   maybe_add_import_meta_filename_dirname(scope, meta, &name);
@@ -480,7 +506,8 @@ fn maybe_add_import_meta_filename_dirname(
   let Some(filename_val) = v8::String::new(scope, &escaped_filename) else {
     return;
   };
-  let filename_key = v8_static_strings::new(scope, v8_static_strings::FILENAME);
+  let filename_key =
+    v8_static_strings::new(scope, &v8_static_strings::FILENAME);
   meta.create_data_property(scope, filename_key.into(), filename_val.into());
 
   let dir_path = file_path
@@ -491,7 +518,7 @@ fn maybe_add_import_meta_filename_dirname(
   let Some(dirname_val) = v8::String::new(scope, &escaped_dirname) else {
     return;
   };
-  let dirname_key = v8_static_strings::new(scope, v8_static_strings::DIRNAME);
+  let dirname_key = v8_static_strings::new(scope, &v8_static_strings::DIRNAME);
   meta.create_data_property(scope, dirname_key.into(), dirname_val.into());
 }
 
@@ -573,7 +600,7 @@ fn catch_dynamic_import_promise_error(
       let msg = v8::Exception::create_message(scope, arg);
       let arg: v8::Local<v8::Object> = arg.try_into().unwrap();
       let message_key =
-        v8_static_strings::new(scope, v8_static_strings::MESSAGE);
+        v8_static_strings::new(scope, &v8_static_strings::MESSAGE);
       let message = arg.get(scope, message_key.into()).unwrap();
       let mut message: v8::Local<v8::String> = message.try_into().unwrap();
       if let Some(stack_frame) = JsStackFrame::from_v8_message(scope, msg) {
@@ -590,9 +617,9 @@ fn catch_dynamic_import_promise_error(
         "ReferenceError" => v8::Exception::reference_error(scope, message),
         _ => v8::Exception::error(scope, message),
       };
-      let code_key = v8_static_strings::new(scope, v8_static_strings::CODE);
+      let code_key = v8_static_strings::new(scope, &v8_static_strings::CODE);
       let code_value =
-        v8_static_strings::new(scope, v8_static_strings::ERR_MODULE_NOT_FOUND);
+        v8_static_strings::new(scope, &v8_static_strings::ERR_MODULE_NOT_FOUND);
       let exception_obj = exception.to_object(scope).unwrap();
       exception_obj.set(scope, code_key.into(), code_value.into());
       scope.throw_exception(exception);
@@ -738,20 +765,23 @@ pub fn create_exports_for_ops_virtual_module<'s>(
 ) -> Vec<(FastString, v8::Local<'s, v8::Value>)> {
   let mut exports = Vec::with_capacity(op_ctxs.len());
 
-  let deno_obj = get(scope, global, v8_static_strings::DENO, "Deno");
+  let deno_obj = get(scope, global, &v8_static_strings::DENO, "Deno");
   let deno_core_obj =
-    get(scope, deno_obj, v8_static_strings::CORE, "Deno.core");
+    get(scope, deno_obj, &v8_static_strings::CORE, "Deno.core");
   let set_up_async_stub_fn: v8::Local<v8::Function> = get(
     scope,
     deno_core_obj,
-    v8_static_strings::SET_UP_ASYNC_STUB,
+    &v8_static_strings::SET_UP_ASYNC_STUB,
     "Deno.core.setUpAsyncStub",
   );
 
   let undefined = v8::undefined(scope);
 
   for op_ctx in op_ctxs {
-    let name = v8_static_strings::new(scope, op_ctx.decl.name.as_bytes());
+    let name = v8_static_strings::new_from_static_str(
+      scope,
+      op_ctx.decl.name.as_bytes(),
+    );
     let mut op_fn = op_ctx_function(scope, op_ctx);
 
     // For async ops we need to set them up, by calling `Deno.core.setUpAsyncStub` -
diff --git a/core/runtime/jsruntime.rs b/core/runtime/jsruntime.rs
index b36650e..6b594d4 100644
--- a/core/runtime/jsruntime.rs
+++ b/core/runtime/jsruntime.rs
@@ -36,8 +36,8 @@ use crate::ops_metrics::OpMetricsFactoryFn;
 use crate::runtime::ContextState;
 use crate::runtime::JsRealm;
 use crate::runtime::OpDriverImpl;
-use crate::source_map::SourceMapCache;
 use crate::source_map::SourceMapGetter;
+use crate::source_map::SourceMapper;
 use crate::Extension;
 use crate::ExtensionFileSource;
 use crate::ExtensionFileSourceCode;
@@ -375,8 +375,7 @@ pub type CompiledWasmModuleStore = CrossIsolateStore<v8::CompiledWasmModule>;
 /// Internal state for JsRuntime which is stored in one of v8::Isolate's
 /// embedder slots.
 pub struct JsRuntimeState {
-  pub(crate) source_map_getter: Option<Rc<Box<dyn SourceMapGetter>>>,
-  pub(crate) source_map_cache: Rc<RefCell<SourceMapCache>>,
+  pub(crate) source_mapper: RefCell<SourceMapper<Box<dyn SourceMapGetter>>>,
   pub(crate) op_state: Rc<RefCell<OpState>>,
   pub(crate) shared_array_buffer_store: Option<SharedArrayBufferStore>,
   pub(crate) compiled_wasm_module_store: Option<CompiledWasmModuleStore>,
@@ -608,8 +607,7 @@ impl JsRuntime {
     let waker = op_state.waker.clone();
     let op_state = Rc::new(RefCell::new(op_state));
     let state_rc = Rc::new(JsRuntimeState {
-      source_map_getter: options.source_map_getter.map(Rc::new),
-      source_map_cache: Default::default(),
+      source_mapper: RefCell::new(SourceMapper::new(options.source_map_getter)),
       shared_array_buffer_store: options.shared_array_buffer_store,
       compiled_wasm_module_store: options.compiled_wasm_module_store,
       wait_for_inspector_disconnect_callback: options
@@ -1127,20 +1125,20 @@ impl JsRuntime {
       // TODO(bartlomieju): these probably could be captured from main realm so we don't have to
       // look up them again?
       let deno_obj: v8::Local<v8::Object> =
-        bindings::get(scope, global, v8_static_strings::DENO, "Deno");
+        bindings::get(scope, global, &v8_static_strings::DENO, "Deno");
       let core_obj: v8::Local<v8::Object> =
-        bindings::get(scope, deno_obj, v8_static_strings::CORE, "Deno.core");
+        bindings::get(scope, deno_obj, &v8_static_strings::CORE, "Deno.core");
 
       let event_loop_tick_cb: v8::Local<v8::Function> = bindings::get(
         scope,
         core_obj,
-        v8_static_strings::EVENT_LOOP_TICK,
+        &v8_static_strings::EVENT_LOOP_TICK,
         "Deno.core.eventLoopTick",
       );
       let build_custom_error_cb: v8::Local<v8::Function> = bindings::get(
         scope,
         core_obj,
-        v8_static_strings::BUILD_CUSTOM_ERROR,
+        &v8_static_strings::BUILD_CUSTOM_ERROR,
         "Deno.core.buildCustomError",
       );
 
@@ -1151,27 +1149,27 @@ impl JsRuntime {
         let web_assembly_object: v8::Local<v8::Object> = bindings::get(
           scope,
           global,
-          v8_static_strings::WEBASSEMBLY,
+          &v8_static_strings::WEBASSEMBLY,
           "WebAssembly",
         );
         let web_assembly_module_object: v8::Local<v8::Object> = bindings::get(
           scope,
           web_assembly_object,
-          v8_static_strings::MODULE,
+          &v8_static_strings::MODULE,
           "WebAssembly.Module",
         );
         web_assembly_module_imports_fn =
           Some(bindings::get::<v8::Local<v8::Function>>(
             scope,
             web_assembly_module_object,
-            v8_static_strings::IMPORTS,
+            &v8_static_strings::IMPORTS,
             "WebAssembly.Module.imports",
           ));
         web_assembly_module_exports_fn =
           Some(bindings::get::<v8::Local<v8::Function>>(
             scope,
             web_assembly_module_object,
-            v8_static_strings::EXPORTS,
+            &v8_static_strings::EXPORTS,
             "WebAssembly.Module.exports",
           ));
       }
diff --git a/core/source_map.rs b/core/source_map.rs
index c695f53..5c57f11 100644
--- a/core/source_map.rs
+++ b/core/source_map.rs
@@ -4,9 +4,7 @@
 
 use crate::resolve_url;
 pub use sourcemap::SourceMap;
-use std::borrow::Cow;
 use std::collections::HashMap;
-use std::rc::Rc;
 use std::str;
 
 pub trait SourceMapGetter {
@@ -19,9 +17,9 @@ pub trait SourceMapGetter {
   ) -> Option<String>;
 }
 
-impl<T> SourceMapGetter for Rc<T>
+impl<T> SourceMapGetter for Box<T>
 where
-  T: SourceMapGetter,
+  T: SourceMapGetter + ?Sized,
 {
   fn get_source_map(&self, file_name: &str) -> Option<Vec<u8>> {
     (**self).get_source_map(file_name)
@@ -36,15 +34,6 @@ where
   }
 }
 
-#[derive(Debug, Default)]
-pub struct SourceMapCache {
-  maps: HashMap<String, Option<SourceMap>>,
-  source_lines: HashMap<(String, i64), Option<String>>,
-  // This is not the right place for this, but it's the easiest way to make
-  // op_apply_source_map a fast op. This stashing should happen in #[op2].
-  pub stashed_file_name: Option<String>,
-}
-
 pub enum SourceMapApplication {
   /// No mapping was applied, the location is unchanged.
   Unchanged,
@@ -61,97 +50,111 @@ pub enum SourceMapApplication {
   },
 }
 
-/// Apply a source map to the passed location. If there is no source map for
-/// this location, or if the location remains unchanged after mapping, the
-/// changed values are returned.
-///
-/// Line and column numbers are 1-based.
-pub fn apply_source_map<G: SourceMapGetter + ?Sized>(
-  file_name: &str,
-  line_number: u32,
-  column_number: u32,
-  cache: &mut SourceMapCache,
-  getter: &G,
-) -> SourceMapApplication {
-  // Lookup expects 0-based line and column numbers, but ours are 1-based.
-  let line_number = line_number - 1;
-  let column_number = column_number - 1;
-
-  let maybe_source_map_entry = cache.maps.get(file_name);
-  let maybe_source_map = maybe_source_map_entry
-    .map(Cow::Borrowed)
-    .unwrap_or_else(|| {
-      let maybe_source_map = getter
-        .get_source_map(file_name)
-        .and_then(|raw_source_map| SourceMap::from_slice(&raw_source_map).ok());
-      Cow::Owned(maybe_source_map)
-    });
-
-  let Some(source_map) = maybe_source_map.as_ref() else {
-    return SourceMapApplication::Unchanged;
-  };
-
-  let Some(token) = source_map.lookup_token(line_number, column_number) else {
-    return SourceMapApplication::Unchanged;
-  };
-
-  let new_line_number = token.get_src_line() + 1;
-  let new_column_number = token.get_src_col() + 1;
-
-  let new_file_name = match token.get_source() {
-    Some(source_file_name) => {
-      if source_file_name == file_name {
-        None
-      } else {
-        // The `source_file_name` written by tsc in the source map is
-        // sometimes only the basename of the URL, or has unwanted `<`/`>`
-        // around it. Use the `file_name` we get from V8 if
-        // `source_file_name` does not parse as a URL.
-        match resolve_url(source_file_name) {
-          Ok(m) if m.scheme() == "blob" => None,
-          Ok(m) => Some(m.to_string()),
-          Err(_) => None,
-        }
-      }
-    }
-    None => None,
-  };
+pub struct SourceMapper<G: SourceMapGetter> {
+  maps: HashMap<String, Option<SourceMap>>,
+  source_lines: HashMap<(String, i64), Option<String>>,
+  getter: Option<G>,
+  // This is not the right place for this, but it's the easiest way to make
+  // op_apply_source_map a fast op. This stashing should happen in #[op2].
+  pub(crate) stashed_file_name: Option<String>,
+}
 
-  match maybe_source_map {
-    Cow::Borrowed(_) => {}
-    Cow::Owned(maybe_source_map) => {
-      cache.maps.insert(file_name.to_owned(), maybe_source_map);
+impl<G: SourceMapGetter> SourceMapper<G> {
+  pub fn new(getter: Option<G>) -> Self {
+    Self {
+      maps: Default::default(),
+      source_lines: Default::default(),
+      getter,
+      stashed_file_name: Default::default(),
     }
   }
 
-  match new_file_name {
-    None => SourceMapApplication::LineAndColumn {
-      line_number: new_line_number,
-      column_number: new_column_number,
-    },
-    Some(file_name) => SourceMapApplication::LineAndColumnAndFileName {
-      file_name,
-      line_number: new_line_number,
-      column_number: new_column_number,
-    },
+  pub fn has_user_sources(&self) -> bool {
+    self.getter.is_some()
   }
-}
 
-const MAX_SOURCE_LINE_LENGTH: usize = 150;
-
-pub fn get_source_line<G: SourceMapGetter + ?Sized>(
-  file_name: &str,
-  line_number: i64,
-  cache: &mut SourceMapCache,
-  getter: &G,
-) -> Option<String> {
-  cache
-    .source_lines
-    .entry((file_name.to_string(), line_number))
-    .or_insert_with(|| {
-      // Source lookup expects a 0-based line number, ours are 1-based.
-      let s = getter.get_source_line(file_name, (line_number - 1) as usize);
-      s.filter(|s| s.len() <= MAX_SOURCE_LINE_LENGTH)
-    })
-    .clone()
+  /// Apply a source map to the passed location. If there is no source map for
+  /// this location, or if the location remains unchanged after mapping, the
+  /// changed values are returned.
+  ///
+  /// Line and column numbers are 1-based.
+  pub fn apply_source_map(
+    &mut self,
+    file_name: &str,
+    line_number: u32,
+    column_number: u32,
+  ) -> SourceMapApplication {
+    // Lookup expects 0-based line and column numbers, but ours are 1-based.
+    let line_number = line_number - 1;
+    let column_number = column_number - 1;
+
+    let getter = self.getter.as_ref();
+    let maybe_source_map =
+      self.maps.entry(file_name.to_owned()).or_insert_with(|| {
+        let raw_source_map = getter.and_then(|it| it.get_source_map(file_name));
+        SourceMap::from_slice(&raw_source_map?).ok()
+      });
+
+    let Some(source_map) = maybe_source_map.as_ref() else {
+      return SourceMapApplication::Unchanged;
+    };
+
+    let Some(token) = source_map.lookup_token(line_number, column_number)
+    else {
+      return SourceMapApplication::Unchanged;
+    };
+
+    let new_line_number = token.get_src_line() + 1;
+    let new_column_number = token.get_src_col() + 1;
+
+    let new_file_name = match token.get_source() {
+      Some(source_file_name) => {
+        if source_file_name == file_name {
+          None
+        } else {
+          // The `source_file_name` written by tsc in the source map is
+          // sometimes only the basename of the URL, or has unwanted `<`/`>`
+          // around it. Use the `file_name` we get from V8 if
+          // `source_file_name` does not parse as a URL.
+          match resolve_url(source_file_name) {
+            Ok(m) if m.scheme() == "blob" => None,
+            Ok(m) => Some(m.to_string()),
+            Err(_) => None,
+          }
+        }
+      }
+      None => None,
+    };
+
+    match new_file_name {
+      None => SourceMapApplication::LineAndColumn {
+        line_number: new_line_number,
+        column_number: new_column_number,
+      },
+      Some(file_name) => SourceMapApplication::LineAndColumnAndFileName {
+        file_name,
+        line_number: new_line_number,
+        column_number: new_column_number,
+      },
+    }
+  }
+
+  const MAX_SOURCE_LINE_LENGTH: usize = 150;
+
+  pub fn get_source_line(
+    &mut self,
+    file_name: &str,
+    line_number: i64,
+  ) -> Option<String> {
+    let getter = self.getter.as_ref()?;
+    self
+      .source_lines
+      .entry((file_name.to_string(), line_number))
+      .or_insert_with(|| {
+        // Source lookup expects a 0-based line number, ours are 1-based.
+        let s = getter.get_source_line(file_name, (line_number - 1) as usize);
+        s.filter(|s| s.len() <= Self::MAX_SOURCE_LINE_LENGTH)
+      })
+      .clone()
+  }
 }


Test patch:
diff --git a/core/runtime/tests/error.rs b/core/runtime/tests/error.rs
index bf064fb..8f76211 100644
--- a/core/runtime/tests/error.rs
+++ b/core/runtime/tests/error.rs
@@ -3,7 +3,6 @@ use crate::error::custom_error;
 use crate::error::JsError;
 use crate::op2;
 use crate::JsRuntime;
-use crate::PollEventLoopOptions;
 use crate::RuntimeOptions;
 use anyhow::Error;
 use futures::future::poll_fn;
@@ -42,69 +41,6 @@ async fn test_error_builder() {
   .await;
 }
 
-#[tokio::test]
-async fn test_error_context() {
-  use anyhow::anyhow;
-
-  #[op2(fast)]
-  fn op_err_sync() -> Result<(), Error> {
-    Err(anyhow!("original sync error").context("higher-level sync error"))
-  }
-
-  #[op2(async)]
-  async fn op_err_async() -> Result<(), Error> {
-    Err(anyhow!("original async error").context("higher-level async error"))
-  }
-
-  deno_core::extension!(test_ext, ops = [op_err_sync, op_err_async]);
-  let mut runtime = JsRuntime::new(RuntimeOptions {
-    extensions: vec![test_ext::init_ops()],
-    ..Default::default()
-  });
-
-  runtime
-    .execute_script_static(
-      "test_error_context_sync.js",
-      r#"
-let errMessage;
-try {
-  Deno.core.ops.op_err_sync();
-} catch (err) {
-  errMessage = err.message;
-}
-if (errMessage !== "higher-level sync error: original sync error") {
-  throw new Error("unexpected error message from op_err_sync: " + errMessage);
-}
-"#,
-    )
-    .unwrap();
-
-  let promise = runtime
-    .execute_script_static(
-      "test_error_context_async.js",
-      r#"
-(async () => {
-  let errMessage;
-  const { op_err_async } = Deno.core.ensureFastOps(); 
-  try {
-    await op_err_async();
-  } catch (err) {
-    errMessage = err.message;
-  }
-  if (errMessage !== "higher-level async error: original async error") {
-    throw new Error("unexpected error message from op_err_async: " + errMessage);
-  }
-})()
-"#,
-    ).unwrap();
-
-  let resolve = runtime.resolve(promise);
-  runtime
-    .with_event_loop_promise(resolve, PollEventLoopOptions::default())
-    .await
-    .unwrap();
-}
-
 #[test]
 fn syntax_error() {
   let mut runtime = JsRuntime::new(Default::default());
diff --git a/testing/checkin/runner/mod.rs b/testing/checkin/runner/mod.rs
index f6267ed..1105329 100644
--- a/testing/checkin/runner/mod.rs
+++ b/testing/checkin/runner/mod.rs
@@ -24,6 +24,7 @@ use self::testing::TestFunctions;
 mod ops;
 mod ops_async;
 mod ops_buffer;
+mod ops_error;
 mod ops_io;
 mod ops_worker;
 mod testing;
@@ -43,9 +44,12 @@ deno_core::extension!(
     ops_async::op_async_yield,
     ops_async::op_async_barrier_create,
     ops_async::op_async_barrier_await,
-    ops_async::op_async_throw_error_eager,
-    ops_async::op_async_throw_error_lazy,
-    ops_async::op_async_throw_error_deferred,
+    ops_error::op_async_throw_error_eager,
+    ops_error::op_async_throw_error_lazy,
+    ops_error::op_async_throw_error_deferred,
+    ops_error::op_error_custom_sync,
+    ops_error::op_error_context_sync,
+    ops_error::op_error_context_async,
     ops_buffer::op_v8slice_store,
     ops_buffer::op_v8slice_clone,
     ops_worker::op_worker_spawn,
@@ -62,6 +66,7 @@ deno_core::extension!(
     "__init.js",
     "async.ts" with_specifier "checkin:async",
     "console.ts" with_specifier "checkin:console",
+    "error.ts" with_specifier "checkin:error",
     "testing.ts" with_specifier "checkin:testing",
     "timers.ts" with_specifier "checkin:timers",
     "worker.ts" with_specifier "checkin:worker",
@@ -95,7 +100,7 @@ fn create_runtime(
       ts_module_loader::TypescriptModuleLoader::default(),
     )),
     get_error_class_fn: Some(&|error| {
-      deno_core::error::get_custom_error_class(error).unwrap()
+      deno_core::error::get_custom_error_class(error).unwrap_or("Error")
     }),
     shared_array_buffer_store: Some(CrossIsolateStore::default()),
     ..Default::default()
diff --git a/testing/checkin/runner/ops_async.rs b/testing/checkin/runner/ops_async.rs
index 0f51a39..0e45834 100644
--- a/testing/checkin/runner/ops_async.rs
+++ b/testing/checkin/runner/ops_async.rs
@@ -1,6 +1,4 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
-use anyhow::Error;
-use deno_core::error::type_error;
 use deno_core::op2;
 use std::future::Future;
 use std::rc::Rc;
@@ -33,18 +31,3 @@ pub fn op_async_barrier_await(
     barrier.wait().await;
   }
 }
-
-#[op2(async)]
-pub async fn op_async_throw_error_eager() -> Result<(), Error> {
-  Err(type_error("Error"))
-}
-
-#[op2(async(deferred), fast)]
-pub async fn op_async_throw_error_deferred() -> Result<(), Error> {
-  Err(type_error("Error"))
-}
-
-#[op2(async(lazy), fast)]
-pub async fn op_async_throw_error_lazy() -> Result<(), Error> {
-  Err(type_error("Error"))
-}
diff --git a/testing/checkin/runner/ops_error.rs b/testing/checkin/runner/ops_error.rs
new file mode 100644
index 0000000..df8aa11
--- /dev/null
+++ b/testing/checkin/runner/ops_error.rs
@@ -0,0 +1,42 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+use anyhow::anyhow;
+use anyhow::Error;
+use deno_core::error::custom_error;
+use deno_core::error::type_error;
+use deno_core::op2;
+
+#[op2(async)]
+pub async fn op_async_throw_error_eager() -> Result<(), Error> {
+  Err(type_error("Error"))
+}
+
+#[op2(async(deferred), fast)]
+pub async fn op_async_throw_error_deferred() -> Result<(), Error> {
+  Err(type_error("Error"))
+}
+
+#[op2(async(lazy), fast)]
+pub async fn op_async_throw_error_lazy() -> Result<(), Error> {
+  Err(type_error("Error"))
+}
+
+#[op2(fast)]
+pub fn op_error_custom_sync(#[string] message: String) -> Result<(), Error> {
+  Err(custom_error("BadResource", message))
+}
+
+#[op2(fast)]
+pub fn op_error_context_sync(
+  #[string] message: String,
+  #[string] context: String,
+) -> Result<(), Error> {
+  Err(anyhow!(message).context(context))
+}
+
+#[op2(async)]
+pub async fn op_error_context_async(
+  #[string] message: String,
+  #[string] context: String,
+) -> Result<(), Error> {
+  Err(anyhow!(message).context(context))
+}
diff --git a/testing/checkin/runtime/__init.js b/testing/checkin/runtime/__init.js
index 5dbce97..5be1d98 100644
--- a/testing/checkin/runtime/__init.js
+++ b/testing/checkin/runtime/__init.js
@@ -3,10 +3,12 @@ import "ext:checkin_runtime/__bootstrap.js";
 import * as async from "checkin:async";
 import * as testing from "checkin:testing";
 import * as console from "checkin:console";
+import * as error from "checkin:error";
 import * as timers from "checkin:timers";
 import * as worker from "checkin:worker";
 testing;
 async;
+error;
 
 globalThis.console = console.console;
 globalThis.setTimeout = timers.setTimeout;
@@ -47,3 +49,6 @@ Reflect.defineProperty(globalThis, "onrejectionhandled", {
 });
 Deno.unrefTimer = timers.unrefTimer;
 Deno.refTimer = timers.refTimer;
+
+// TODO(mmastrac): arguably this could be registered in 00_infra.js since it's a deno_core error
+Deno.core.registerErrorClass("BadResource", Deno.core.BadResource);
diff --git a/testing/checkin/runtime/async.ts b/testing/checkin/runtime/async.ts
index 026fa96..a4d9fd2 100644
--- a/testing/checkin/runtime/async.ts
+++ b/testing/checkin/runtime/async.ts
@@ -3,9 +3,6 @@ const {
   op_async_barrier_create,
   op_async_barrier_await,
   op_async_yield,
-  op_async_throw_error_eager,
-  op_async_throw_error_lazy,
-  op_async_throw_error_deferred,
   op_stats_capture,
   op_stats_diff,
   op_stats_dump,
@@ -14,15 +11,6 @@ const {
   .core
   .ensureFastOps();
 
-export async function asyncThrow(kind: "lazy" | "eager" | "deferred") {
-  const op = {
-    lazy: op_async_throw_error_lazy,
-    eager: op_async_throw_error_eager,
-    deferred: op_async_throw_error_deferred,
-  }[kind];
-  return await op();
-}
-
 export function barrierCreate(name: string, count: number) {
   op_async_barrier_create(name, count);
 }
diff --git a/testing/checkin/runtime/error.ts b/testing/checkin/runtime/error.ts
new file mode 100644
index 0000000..97081a0
--- /dev/null
+++ b/testing/checkin/runtime/error.ts
@@ -0,0 +1,33 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+import {
+  op_async_throw_error_deferred,
+  op_async_throw_error_eager,
+  op_async_throw_error_lazy,
+  op_error_context_async,
+  op_error_context_sync,
+  op_error_custom_sync,
+} from "ext:core/ops";
+
+export async function asyncThrow(kind: "lazy" | "eager" | "deferred") {
+  const op = {
+    lazy: op_async_throw_error_lazy,
+    eager: op_async_throw_error_eager,
+    deferred: op_async_throw_error_deferred,
+  }[kind];
+  return await op();
+}
+
+export function throwCustomError(message: string) {
+  op_error_custom_sync(message);
+}
+
+export function throwErrorWithContextSync(message: string, context: string) {
+  op_error_context_sync(message, context);
+}
+
+export async function throwErrorWithContextAsync(
+  message: string,
+  context: string,
+) {
+  await op_error_context_async(message, context);
+}
diff --git a/testing/lib.rs b/testing/lib.rs
index 20e742e..09605aa 100644
--- a/testing/lib.rs
+++ b/testing/lib.rs
@@ -34,6 +34,7 @@ macro_rules! integration_test {
 // Test individual bits of functionality. These files are loaded from the unit/ dir.
 unit_test!(
   encode_decode_test,
+  error_test,
   microtask_test,
   ops_async_test,
   ops_buffer_test,
diff --git a/testing/tsconfig.json b/testing/tsconfig.json
index 95250fd..b997ba5 100644
--- a/testing/tsconfig.json
+++ b/testing/tsconfig.json
@@ -5,11 +5,15 @@
     "paths": {
       "checkin:async": ["checkin/runtime/async.ts"],
       "checkin:console": ["checkin/runtime/console.ts"],
+      "checkin:error": ["checkin/runtime/error.ts"],
       "checkin:testing": ["checkin/runtime/testing.ts"],
+      "checkin:timers": ["checkin/runtime/timers.ts"],
       "checkin:worker": ["checkin/runtime/worker.ts"]
     },
     "lib": ["ESNext", "DOM", "ES2023.Array"],
     "module": "ES2022",
-    "moduleDetection": "force"
+    "moduleDetection": "force",
+    "noEmit": true,
+    "allowImportingTsExtensions": true
   }
 }
diff --git a/testing/unit/error_test.ts b/testing/unit/error_test.ts
new file mode 100644
index 0000000..88a51c3
--- /dev/null
+++ b/testing/unit/error_test.ts
@@ -0,0 +1,32 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+import {
+  throwCustomError,
+  throwErrorWithContextAsync,
+  throwErrorWithContextSync,
+} from "checkin:error";
+import { assert, assertEquals, test } from "checkin:testing";
+
+test(function testSyncContext() {
+  try {
+    throwErrorWithContextSync("message", "with context");
+  } catch (e) {
+    assertEquals(e.message, "with context: message");
+  }
+});
+
+test(async function testAsyncContext() {
+  try {
+    await throwErrorWithContextAsync("message", "with context");
+  } catch (e) {
+    assertEquals(e.message, "with context: message");
+  }
+});
+
+test(function testCustomError() {
+  try {
+    throwCustomError("uh oh");
+  } catch (e) {
+    assertEquals(e.message, "uh oh");
+    assert(e instanceof Deno.core.BadResource);
+  }
+});
diff --git a/testing/unit/ops_async_test.ts b/testing/unit/ops_async_test.ts
index 59c03f1..7806b1a 100644
--- a/testing/unit/ops_async_test.ts
+++ b/testing/unit/ops_async_test.ts
@@ -1,11 +1,7 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
 import { assertEquals, test } from "checkin:testing";
-import {
-  asyncThrow,
-  asyncYield,
-  barrierAwait,
-  barrierCreate,
-} from "checkin:async";
+import { asyncYield, barrierAwait, barrierCreate } from "checkin:async";
+import { asyncThrow } from "checkin:error";
 
 // Test that stack traces from async ops are all sane
 test(async function testAsyncThrow() {
@@ -15,7 +11,7 @@ test(async function testAsyncThrow() {
     const stack = e.stack.replace(/\d+:\d+/g, "line:col");
     assertEquals(
       stack,
-      "TypeError: Error\n    at asyncThrow (checkin:async:line:col)\n    at testAsyncThrow (test:///unit/ops_async_test.ts:line:col)",
+      "TypeError: Error\n    at asyncThrow (checkin:error:line:col)\n    at testAsyncThrow (test:///unit/ops_async_test.ts:line:col)",
     );
   }
   try {
@@ -24,7 +20,7 @@ test(async function testAsyncThrow() {
     const stack = e.stack.replace(/\d+:\d+/g, "line:col");
     assertEquals(
       stack,
-      "TypeError: Error\n    at async asyncThrow (checkin:async:line:col)\n    at async testAsyncThrow (test:///unit/ops_async_test.ts:line:col)",
+      "TypeError: Error\n    at async asyncThrow (checkin:error:line:col)\n    at async testAsyncThrow (test:///unit/ops_async_test.ts:line:col)",
     );
   }
   try {
@@ -33,7 +29,7 @@ test(async function testAsyncThrow() {
     const stack = e.stack.replace(/\d+:\d+/g, "line:col");
     assertEquals(
       stack,
-      "TypeError: Error\n    at async asyncThrow (checkin:async:line:col)\n    at async testAsyncThrow (test:///unit/ops_async_test.ts:line:col)",
+      "TypeError: Error\n    at async asyncThrow (checkin:error:line:col)\n    at async testAsyncThrow (test:///unit/ops_async_test.ts:line:col)",
     );
   }
 });
diff --git a/testing/unit/resource_test.ts b/testing/unit/resource_test.ts
index 44ddf21..344fbac 100644
--- a/testing/unit/resource_test.ts
+++ b/testing/unit/resource_test.ts
@@ -1,5 +1,5 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
-import { assertArrayEquals, assertEquals, test } from "checkin:testing";
+import { assert, assertArrayEquals, assertEquals, test } from "checkin:testing";
 
 const { op_pipe_create } = Deno.core.ensureFastOps();
 
@@ -23,3 +23,27 @@ test(async function testPipeSmallRead() {
     assertArrayEquals(buf.subarray(0), [i]);
   }
 });
+
+test(async function opsAsyncBadResource() {
+  try {
+    const nonExistingRid = 9999;
+    await Deno.core.read(
+      nonExistingRid,
+      new Uint8Array(100),
+    );
+  } catch (e) {
+    assert(e instanceof Deno.core.BadResource);
+  }
+});
+
+test(function opsSyncBadResource() {
+  try {
+    const nonExistingRid = 9999;
+    Deno.core.readSync(
+      nonExistingRid,
+      new Uint8Array(100),
+    );
+  } catch (e) {
+    assert(e instanceof Deno.core.BadResource);
+  }
+});