diff --git a/BUILD.gn b/BUILD.gn
index 1696c2483b..7c29e42583 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -91,6 +91,7 @@ ts_sources = [
   "js/os.ts",
   "js/platform.ts",
   "js/plugins.d.ts",
+  "js/promise_util.ts",
   "js/read_dir.ts",
   "js/read_file.ts",
   "js/read_link.ts",
diff --git a/js/libdeno.ts b/js/libdeno.ts
index 97269c36c9..842b0c8ade 100644
--- a/js/libdeno.ts
+++ b/js/libdeno.ts
@@ -3,6 +3,12 @@ import { globalEval } from "./global_eval";
 
 // The libdeno functions are moved so that users can't access them.
 type MessageCallback = (msg: Uint8Array) => void;
+export type PromiseRejectEvent =
+  | "RejectWithNoHandler"
+  | "HandlerAddedAfterReject"
+  | "ResolveAfterResolved"
+  | "RejectAfterResolved";
+
 interface Libdeno {
   recv(cb: MessageCallback): void;
 
@@ -20,6 +26,17 @@ interface Libdeno {
     ) => void
   ) => void;
 
+  setPromiseRejectHandler: (
+    handler: (
+      error: Error | string,
+      event: PromiseRejectEvent,
+      /* tslint:disable-next-line:no-any */
+      promise: Promise<any>
+    ) => void
+  ) => void;
+
+  setPromiseErrorExaminer: (handler: () => boolean) => void;
+
   mainSource: string;
   mainSourceMap: RawSourceMap;
 }
diff --git a/js/main.ts b/js/main.ts
index 24418b53e4..0d33cf063e 100644
--- a/js/main.ts
+++ b/js/main.ts
@@ -7,6 +7,7 @@ import { DenoCompiler } from "./compiler";
 import { libdeno } from "./libdeno";
 import { args } from "./deno";
 import { sendSync, handleAsyncMsgFromRust } from "./dispatch";
+import { promiseErrorExaminer, promiseRejectHandler } from "./promise_util";
 
 function sendStart(): msg.StartRes {
   const builder = new flatbuffers.Builder();
@@ -39,6 +40,8 @@ function onGlobalError(
 export default function denoMain() {
   libdeno.recv(handleAsyncMsgFromRust);
   libdeno.setGlobalErrorHandler(onGlobalError);
+  libdeno.setPromiseRejectHandler(promiseRejectHandler);
+  libdeno.setPromiseErrorExaminer(promiseErrorExaminer);
   const compiler = DenoCompiler.instance();
 
   // First we send an empty "Start" message to let the privileged side know we
diff --git a/js/promise_util.ts b/js/promise_util.ts
new file mode 100644
index 0000000000..a550bc3886
--- /dev/null
+++ b/js/promise_util.ts
@@ -0,0 +1,46 @@
+import { PromiseRejectEvent } from "./libdeno";
+
+/* tslint:disable-next-line:no-any */
+const rejectMap = new Map<Promise<any>, string>();
+// For uncaught promise rejection errors
+
+/* tslint:disable-next-line:no-any */
+const otherErrorMap = new Map<Promise<any>, string>();
+// For reject after resolve / resolve after resolve errors
+
+export function promiseRejectHandler(
+  error: Error | string,
+  event: PromiseRejectEvent,
+  /* tslint:disable-next-line:no-any */
+  promise: Promise<any>
+) {
+  switch (event) {
+    case "RejectWithNoHandler":
+      rejectMap.set(promise, (error as Error).stack || "RejectWithNoHandler");
+      break;
+    case "HandlerAddedAfterReject":
+      rejectMap.delete(promise);
+      break;
+    default:
+      // error is string here
+      otherErrorMap.set(promise, `Promise warning: ${error as string}`);
+  }
+}
+
+// Return true when continue, false to die on uncaught promise reject
+export function promiseErrorExaminer(): boolean {
+  if (otherErrorMap.size > 0) {
+    for (const msg of otherErrorMap.values()) {
+      console.log(msg);
+    }
+    otherErrorMap.clear();
+  }
+  if (rejectMap.size > 0) {
+    for (const msg of rejectMap.values()) {
+      console.log(msg);
+    }
+    rejectMap.clear();
+    return false;
+  }
+  return true;
+}
diff --git a/libdeno/binding.cc b/libdeno/binding.cc
index c5f582aaee..37d358d78d 100644
--- a/libdeno/binding.cc
+++ b/libdeno/binding.cc
@@ -138,15 +138,59 @@ void HandleException(v8::Local<v8::Context> context,
   }
 }
 
-void ExitOnPromiseRejectCallback(
-    v8::PromiseRejectMessage promise_reject_message) {
+const char* PromiseRejectStr(enum v8::PromiseRejectEvent e) {
+  switch (e) {
+    case v8::PromiseRejectEvent::kPromiseRejectWithNoHandler:
+      return "RejectWithNoHandler";
+    case v8::PromiseRejectEvent::kPromiseHandlerAddedAfterReject:
+      return "HandlerAddedAfterReject";
+    case v8::PromiseRejectEvent::kPromiseResolveAfterResolved:
+      return "ResolveAfterResolved";
+    case v8::PromiseRejectEvent::kPromiseRejectAfterResolved:
+      return "RejectAfterResolved";
+  }
+}
+
+void PromiseRejectCallback(v8::PromiseRejectMessage promise_reject_message) {
   auto* isolate = v8::Isolate::GetCurrent();
   Deno* d = static_cast<Deno*>(isolate->GetData(0));
   DCHECK_EQ(d->isolate, isolate);
   v8::HandleScope handle_scope(d->isolate);
   auto exception = promise_reject_message.GetValue();
   auto context = d->context.Get(d->isolate);
-  HandleException(context, exception);
+  auto promise = promise_reject_message.GetPromise();
+  auto event = promise_reject_message.GetEvent();
+
+  v8::Context::Scope context_scope(context);
+  auto promise_reject_handler = d->promise_reject_handler.Get(isolate);
+
+  if (!promise_reject_handler.IsEmpty()) {
+    v8::Local<v8::Value> args[3];
+    args[1] = v8_str(PromiseRejectStr(event));
+    args[2] = promise;
+    /* error, event, promise */
+    if (event == v8::PromiseRejectEvent::kPromiseRejectWithNoHandler) {
+      d->pending_promise_events++;
+      // exception only valid for kPromiseRejectWithNoHandler
+      args[0] = exception;
+    } else if (event ==
+               v8::PromiseRejectEvent::kPromiseHandlerAddedAfterReject) {
+      d->pending_promise_events--;  // unhandled event cancelled
+      if (d->pending_promise_events < 0) {
+        d->pending_promise_events = 0;
+      }
+      // Placeholder, not actually used
+      args[0] = v8_str("Promise handler added");
+    } else if (event == v8::PromiseRejectEvent::kPromiseResolveAfterResolved) {
+      d->pending_promise_events++;
+      args[0] = v8_str("Promise resolved after resolved");
+    } else if (event == v8::PromiseRejectEvent::kPromiseRejectAfterResolved) {
+      d->pending_promise_events++;
+      args[0] = v8_str("Promise rejected after resolved");
+    }
+    promise_reject_handler->Call(context->Global(), 3, args);
+    return;
+  }
 }
 
 void Print(const v8::FunctionCallbackInfo<v8::Value>& args) {
@@ -279,6 +323,48 @@ void SetGlobalErrorHandler(const v8::FunctionCallbackInfo<v8::Value>& args) {
   d->global_error_handler.Reset(isolate, func);
 }
 
+// Sets the promise uncaught reject handler
+void SetPromiseRejectHandler(const v8::FunctionCallbackInfo<v8::Value>& args) {
+  v8::Isolate* isolate = args.GetIsolate();
+  Deno* d = reinterpret_cast<Deno*>(isolate->GetData(0));
+  DCHECK_EQ(d->isolate, isolate);
+
+  v8::HandleScope handle_scope(isolate);
+
+  if (!d->promise_reject_handler.IsEmpty()) {
+    isolate->ThrowException(
+        v8_str("libdeno.setPromiseRejectHandler already called."));
+    return;
+  }
+
+  v8::Local<v8::Value> v = args[0];
+  CHECK(v->IsFunction());
+  v8::Local<v8::Function> func = v8::Local<v8::Function>::Cast(v);
+
+  d->promise_reject_handler.Reset(isolate, func);
+}
+
+// Sets the promise uncaught reject handler
+void SetPromiseErrorExaminer(const v8::FunctionCallbackInfo<v8::Value>& args) {
+  v8::Isolate* isolate = args.GetIsolate();
+  Deno* d = reinterpret_cast<Deno*>(isolate->GetData(0));
+  DCHECK_EQ(d->isolate, isolate);
+
+  v8::HandleScope handle_scope(isolate);
+
+  if (!d->promise_error_examiner.IsEmpty()) {
+    isolate->ThrowException(
+        v8_str("libdeno.setPromiseErrorExaminer already called."));
+    return;
+  }
+
+  v8::Local<v8::Value> v = args[0];
+  CHECK(v->IsFunction());
+  v8::Local<v8::Function> func = v8::Local<v8::Function>::Cast(v);
+
+  d->promise_error_examiner.Reset(isolate, func);
+}
+
 bool ExecuteV8StringSource(v8::Local<v8::Context> context,
                            const char* js_filename,
                            v8::Local<v8::String> source) {
@@ -354,6 +440,24 @@ void InitializeContext(v8::Isolate* isolate, v8::Local<v8::Context> context,
                   set_global_error_handler_val)
             .FromJust());
 
+  auto set_promise_reject_handler_tmpl =
+      v8::FunctionTemplate::New(isolate, SetPromiseRejectHandler);
+  auto set_promise_reject_handler_val =
+      set_promise_reject_handler_tmpl->GetFunction(context).ToLocalChecked();
+  CHECK(deno_val
+            ->Set(context, deno::v8_str("setPromiseRejectHandler"),
+                  set_promise_reject_handler_val)
+            .FromJust());
+
+  auto set_promise_error_examiner_tmpl =
+      v8::FunctionTemplate::New(isolate, SetPromiseErrorExaminer);
+  auto set_promise_error_examiner_val =
+      set_promise_error_examiner_tmpl->GetFunction(context).ToLocalChecked();
+  CHECK(deno_val
+            ->Set(context, deno::v8_str("setPromiseErrorExaminer"),
+                  set_promise_error_examiner_val)
+            .FromJust());
+
   {
     auto source = deno::v8_str(js_source.c_str());
     CHECK(
@@ -389,6 +493,7 @@ void InitializeContext(v8::Isolate* isolate, v8::Local<v8::Context> context,
 }
 
 void AddIsolate(Deno* d, v8::Isolate* isolate) {
+  d->pending_promise_events = 0;
   d->next_req_id = 0;
   d->isolate = isolate;
   // Leaving this code here because it will probably be useful later on, but
@@ -397,7 +502,7 @@ void AddIsolate(Deno* d, v8::Isolate* isolate) {
   // d->isolate->SetAbortOnUncaughtExceptionCallback(AbortOnUncaughtExceptionCallback);
   // d->isolate->AddMessageListener(MessageCallback2);
   // d->isolate->SetFatalErrorHandler(FatalErrorCallback2);
-  d->isolate->SetPromiseRejectCallback(deno::ExitOnPromiseRejectCallback);
+  d->isolate->SetPromiseRejectCallback(deno::PromiseRejectCallback);
   d->isolate->SetData(0, d);
 }
 
@@ -490,6 +595,36 @@ int deno_respond(Deno* d, void* user_data, int32_t req_id, deno_buf buf) {
   return 0;
 }
 
+void deno_check_promise_errors(Deno* d) {
+  if (d->pending_promise_events > 0) {
+    auto* isolate = d->isolate;
+    v8::Locker locker(isolate);
+    v8::Isolate::Scope isolate_scope(isolate);
+    v8::HandleScope handle_scope(isolate);
+
+    auto context = d->context.Get(d->isolate);
+    v8::Context::Scope context_scope(context);
+
+    v8::TryCatch try_catch(d->isolate);
+    auto promise_error_examiner = d->promise_error_examiner.Get(d->isolate);
+    if (promise_error_examiner.IsEmpty()) {
+      d->last_exception =
+          "libdeno.setPromiseErrorExaminer has not been called.";
+      return;
+    }
+    v8::Local<v8::Value> args[0];
+    auto result = promise_error_examiner->Call(context->Global(), 0, args);
+    if (try_catch.HasCaught()) {
+      deno::HandleException(context, try_catch.Exception());
+    }
+    d->pending_promise_events = 0;  // reset
+    if (!result->BooleanValue(context).FromJust()) {
+      // Has uncaught promise reject error, exiting...
+      exit(1);
+    }
+  }
+}
+
 void deno_delete(Deno* d) {
   d->isolate->Dispose();
   delete d;
diff --git a/libdeno/deno.h b/libdeno/deno.h
index 5fad6a1c32..7040455249 100644
--- a/libdeno/deno.h
+++ b/libdeno/deno.h
@@ -59,6 +59,8 @@ int deno_execute(Deno* d, void* user_data, const char* js_filename,
 // libdeno.recv() callback. Check deno_last_exception() for exception text.
 int deno_respond(Deno* d, void* user_data, int32_t req_id, deno_buf buf);
 
+void deno_check_promise_errors(Deno* d);
+
 const char* deno_last_exception(Deno* d);
 
 void deno_terminate_execution(Deno* d);
diff --git a/libdeno/internal.h b/libdeno/internal.h
index 08e5cc0f59..f8b587658a 100644
--- a/libdeno/internal.h
+++ b/libdeno/internal.h
@@ -14,6 +14,9 @@ struct deno_s {
   std::string last_exception;
   v8::Persistent<v8::Function> recv;
   v8::Persistent<v8::Function> global_error_handler;
+  v8::Persistent<v8::Function> promise_reject_handler;
+  v8::Persistent<v8::Function> promise_error_examiner;
+  int32_t pending_promise_events;
   v8::Persistent<v8::Context> context;
   v8::Persistent<v8::Map> async_data_map;
   deno_recv_cb cb;
@@ -32,10 +35,16 @@ void Print(const v8::FunctionCallbackInfo<v8::Value>& args);
 void Recv(const v8::FunctionCallbackInfo<v8::Value>& args);
 void Send(const v8::FunctionCallbackInfo<v8::Value>& args);
 void SetGlobalErrorHandler(const v8::FunctionCallbackInfo<v8::Value>& args);
+void SetPromiseRejectHandler(const v8::FunctionCallbackInfo<v8::Value>& args);
+void SetPromiseErrorExaminer(const v8::FunctionCallbackInfo<v8::Value>& args);
 static intptr_t external_references[] = {
-    reinterpret_cast<intptr_t>(Print), reinterpret_cast<intptr_t>(Recv),
+    reinterpret_cast<intptr_t>(Print),
+    reinterpret_cast<intptr_t>(Recv),
     reinterpret_cast<intptr_t>(Send),
-    reinterpret_cast<intptr_t>(SetGlobalErrorHandler), 0};
+    reinterpret_cast<intptr_t>(SetGlobalErrorHandler),
+    reinterpret_cast<intptr_t>(SetPromiseRejectHandler),
+    reinterpret_cast<intptr_t>(SetPromiseErrorExaminer),
+    0};
 
 Deno* NewFromSnapshot(void* user_data, deno_recv_cb cb);
 
diff --git a/libdeno/libdeno_test.cc b/libdeno/libdeno_test.cc
index e46c629786..f79fa70d7a 100644
--- a/libdeno/libdeno_test.cc
+++ b/libdeno/libdeno_test.cc
@@ -193,3 +193,15 @@ TEST(LibDenoTest, DataBuf) {
   EXPECT_EQ(data_buf_copy.data_ptr[1], 8);
   deno_delete(d);
 }
+
+TEST(LibDenoTest, PromiseRejectCatchHandling) {
+  static int count = 0;
+  Deno* d = deno_new([](auto _, int req_id, auto buf, auto data_buf) {
+    // If no error, nothing should be sent, and count should not increment
+    count++;
+  });
+  EXPECT_TRUE(deno_execute(d, nullptr, "a.js", "PromiseRejectCatchHandling()"));
+
+  EXPECT_EQ(count, 0);
+  deno_delete(d);
+}
diff --git a/libdeno/libdeno_test.js b/libdeno/libdeno_test.js
index 1b5137f0ab..1aa09a7752 100644
--- a/libdeno/libdeno_test.js
+++ b/libdeno/libdeno_test.js
@@ -133,3 +133,43 @@ global.DataBuf = () => {
   b[0] = 9;
   b[1] = 8;
 };
+
+global.PromiseRejectCatchHandling = () => {
+  let count = 0;
+  let promiseRef = null;
+  // When we have an error, libdeno sends something
+  function assertOrSend(cond) {
+    if (!cond) {
+      libdeno.send(new Uint8Array([42]));
+    }
+  }
+  libdeno.setPromiseErrorExaminer(() => {
+    assertOrSend(count === 2);
+  });
+  libdeno.setPromiseRejectHandler((error, event, promise) => {
+    count++;
+    if (event === "RejectWithNoHandler") {
+      assertOrSend(error instanceof Error);
+      assertOrSend(error.message === "message");
+      assertOrSend(count === 1);
+      promiseRef = promise;
+    } else if (event === "HandlerAddedAfterReject") {
+      assertOrSend(count === 2);
+      assertOrSend(promiseRef === promise);
+    }
+    // Should never reach 3!
+    assertOrSend(count !== 3);
+  });
+
+  async function fn() {
+    throw new Error("message");
+  }
+
+  (async () => {
+    try {
+      await fn();
+    } catch (e) {
+      assertOrSend(count === 2);
+    }
+  })();
+}
diff --git a/src/isolate.rs b/src/isolate.rs
index 40dff5ed25..5a09b8855c 100644
--- a/src/isolate.rs
+++ b/src/isolate.rs
@@ -195,6 +195,12 @@ impl Isolate {
     }
   }
 
+  fn check_promise_errors(&self) {
+    unsafe {
+      libdeno::deno_check_promise_errors(self.libdeno_isolate);
+    }
+  }
+
   // TODO Use Park abstraction? Note at time of writing Tokio default runtime
   // does not have new_with_park().
   pub fn event_loop(&mut self) {
@@ -205,7 +211,10 @@ impl Isolate {
         Err(mpsc::RecvTimeoutError::Timeout) => self.timeout(),
         Err(e) => panic!("recv_deadline() failed: {:?}", e),
       }
+      self.check_promise_errors();
     }
+    // Check on done
+    self.check_promise_errors();
   }
 
   fn ntasks_increment(&mut self) {
diff --git a/src/libdeno.rs b/src/libdeno.rs
index 4417ac5448..732c78a210 100644
--- a/src/libdeno.rs
+++ b/src/libdeno.rs
@@ -33,6 +33,7 @@ extern "C" {
   pub fn deno_new(cb: DenoRecvCb) -> *const isolate;
   pub fn deno_delete(i: *const isolate);
   pub fn deno_last_exception(i: *const isolate) -> *const c_char;
+  pub fn deno_check_promise_errors(i: *const isolate);
   pub fn deno_respond(
     i: *const isolate,
     user_data: *mut c_void,
diff --git a/tests/018_async_catch.ts b/tests/018_async_catch.ts
new file mode 100644
index 0000000000..b073654d97
--- /dev/null
+++ b/tests/018_async_catch.ts
@@ -0,0 +1,14 @@
+async function fn() {
+  throw new Error("message");
+}
+async function call() {
+  try {
+    console.log("before await fn()");
+    await fn();
+    console.log("after await fn()");
+  } catch (error) {
+    console.log("catch");
+  }
+  console.log("after try-catch");
+}
+call().catch(() => console.log("outer catch"));
diff --git a/tests/018_async_catch.ts.out b/tests/018_async_catch.ts.out
new file mode 100644
index 0000000000..4fc2199735
--- /dev/null
+++ b/tests/018_async_catch.ts.out
@@ -0,0 +1,3 @@
+before await fn()
+catch
+after try-catch
diff --git a/tests/async_error.ts.out b/tests/async_error.ts.out
index 862cf0f963..69be815a09 100644
--- a/tests/async_error.ts.out
+++ b/tests/async_error.ts.out
@@ -1,5 +1,6 @@
 hello
 before error
+world
 Error: error
     at foo ([WILDCARD]tests/async_error.ts:4:9)
     at eval ([WILDCARD]tests/async_error.ts:7:1)