diff --git a/system/lib/libcxx/new.cpp b/system/lib/libcxx/new.cpp
index 901e78565857b..bd0b42f7cb7c0 100644
--- a/system/lib/libcxx/new.cpp
+++ b/system/lib/libcxx/new.cpp
@@ -74,8 +74,17 @@ operator new(std::size_t size) _THROW_BAD_ALLOC
         else
 #ifndef _LIBCPP_NO_EXCEPTIONS
             throw std::bad_alloc();
+#else
+#ifdef __EMSCRIPTEN__
+            // Abort here so that when exceptions are disabled, we do not just
+            // return 0 when malloc returns 0.
+            // We could also do this with set_new_handler, but that adds a
+            // global constructor and a table entry, overhead that we can avoid
+            // by doing it this way.
+            abort();
 #else
             break;
+#endif
 #endif
     }
     return p;
diff --git a/system/lib/libcxxabi/src/stdlib_new_delete.cpp b/system/lib/libcxxabi/src/stdlib_new_delete.cpp
index 698c5f7c290c0..0fcb486ec663a 100644
--- a/system/lib/libcxxabi/src/stdlib_new_delete.cpp
+++ b/system/lib/libcxxabi/src/stdlib_new_delete.cpp
@@ -38,8 +38,17 @@ operator new(std::size_t size) _THROW_BAD_ALLOC
         else
 #ifndef _LIBCXXABI_NO_EXCEPTIONS
             throw std::bad_alloc();
+#else
+#ifdef __EMSCRIPTEN__
+            // Abort here so that when exceptions are disabled, we do not just
+            // return 0 when malloc returns 0.
+            // We could also do this with set_new_handler, but that adds a
+            // global constructor and a table entry, overhead that we can avoid
+            // by doing it this way.
+            abort();
 #else
             break;
+#endif
 #endif
     }
     return p;
diff --git a/tests/core/test_aborting_new.cpp b/tests/core/test_aborting_new.cpp
new file mode 100644
index 0000000000000..a4d4c8b77cbf4
--- /dev/null
+++ b/tests/core/test_aborting_new.cpp
@@ -0,0 +1,23 @@
+#include <emscripten.h>
+#include <stdio.h>
+#include <vector>
+
+EMSCRIPTEN_KEEPALIVE extern "C" void allocate_too_much() {
+  std::vector<int> x;
+  puts("allocating more than TOTAL_MEMORY; this will fail.");
+  x.resize(20 * 1024 * 1024);
+  puts("oh no, it didn't fail!");
+}
+
+int main() {
+  EM_ASM({
+    // Catch the failure here so we can report it.
+    try {
+      _allocate_too_much();
+      out("no abort happened");
+    } catch (e) {
+      assert(("" + e).indexOf("abort") >= 0, "expect an abort from new");
+      out("new aborted as expected");
+    }
+  });
+}
diff --git a/tests/core/test_aborting_new.txt b/tests/core/test_aborting_new.txt
new file mode 100644
index 0000000000000..9ff167b0e499b
--- /dev/null
+++ b/tests/core/test_aborting_new.txt
@@ -0,0 +1 @@
+new aborted as expected
diff --git a/tests/test_core.py b/tests/test_core.py
index 876934937fc55..909a4b8fba675 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -2169,6 +2169,16 @@ def test_memorygrowth_3_force_fail_reallocBuffer(self):
     self.emcc_args += ['-Wno-almost-asm', '-s', 'ALLOW_MEMORY_GROWTH=1', '-s', 'TEST_MEMORY_GROWTH_FAILS=1']
     self.do_run_in_out_file_test('tests', 'core', 'test_memorygrowth_3')
 
+  @parameterized({
+    'nogrow': (['-s', 'ALLOW_MEMORY_GROWTH=0'],),
+    'grow': (['-s', 'ALLOW_MEMORY_GROWTH=1'],)
+  })
+  def test_aborting_new(self, args):
+    # test that C++ new properly errors if we fail to malloc when growth is
+    # enabled, with or without growth
+    self.emcc_args += ['-Wno-almost-asm', '-s', 'MAXIMUM_MEMORY=18MB'] + args
+    self.do_run_in_out_file_test('tests', 'core', 'test_aborting_new')
+
   @no_asmjs()
   @no_wasm2js('no WebAssembly.Memory()')
   @no_asan('ASan alters the memory size')