diff --git a/.travis.yml b/.travis.yml
index 5c44441b..92c3bf98 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,7 +6,7 @@ INSTALL_NODE_VIA_NVM: &INSTALL_NODE_VIA_NVM
     rustup target add wasm32-unknown-unknown
     curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.8/install.sh | bash
     source ~/.nvm/nvm.sh
-    nvm install v10.5
+    nvm install v10.9
 
 INSTALL_GECKODRIVER: &INSTALL_GECKODRIVER
   |
diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs
index c63470ed..97a062f4 100644
--- a/crates/cli-support/src/js/mod.rs
+++ b/crates/cli-support/src/js/mod.rs
@@ -338,6 +338,88 @@ impl<'a> Context<'a> {
             ))
         })?;
 
+        self.bind("__wbindgen_debug_string", &|me| {
+            me.expose_pass_string_to_wasm()?;
+            me.expose_get_object();
+            me.expose_uint32_memory();
+            Ok(String::from(
+                "
+                function(i, len_ptr) {
+                    const toString = Object.prototype.toString;
+                    const debug_str = val => {
+                        // primitive types
+                        const type = typeof val;
+                        if (type == 'number' || type == 'boolean' || val == null) {
+                            return  `${val}`;
+                        }
+                        if (type == 'string') {
+                            return `\"${val}\"`;
+                        }
+                        if (type == 'symbol') {
+                            const description = val.description;
+                            if (description == null) {
+                                return 'Symbol';
+                            } else {
+                                return `Symbol(${description})`;
+                            }
+                        }
+                        if (type == 'function') {
+                            const name = val.name;
+                            if (typeof name == 'string' && name.length > 0) {
+                                return `Function(${name})`;
+                            } else {
+                                return 'Function';
+                            }
+                        }
+                        // objects
+                        if (Array.isArray(val)) {
+                            const length = val.length;
+                            let debug = '[';
+                            if (length > 0) {
+                                debug += debug_str(val[0]);
+                            }
+                            for(let i = 1; i < length; i++) {
+                                debug += ', ' + debug_str(val[i]);
+                            }
+                            debug += ']';
+                            return debug;
+                        }
+                        // Test for built-in
+                        const builtInMatches = /\\[object ([^\\]]+)\\]/.exec(toString.call(val));
+                        let className;
+                        if (builtInMatches.length > 1) {
+                            className = builtInMatches[1];
+                        } else {
+                            // Failed to match the standard '[object ClassName]'
+                            return toString.call(val);
+                        }
+                        if (className == 'Object') {
+                            // we're a user defined class or Object
+                            // JSON.stringify avoids problems with cycles, and is generally much
+                            // easier than looping through ownProperties of `val`.
+                            try {
+                                return 'Object(' + JSON.stringify(val) + ')';
+                            } catch (_) {
+                                return 'Object';
+                            }
+                        }
+                        // errors
+                        if (val instanceof Error) {
+                            return `${val.name}: ${val.message}\n${val.stack}`;
+                        }
+                        // TODO we could test for more things here, like `Set`s and `Map`s.
+                        return className;
+                    };
+                    const val = getObject(i);
+                    const debug = debug_str(val);
+                    const ptr = passStringToWasm(debug);
+                    getUint32Memory()[len_ptr / 4] = WASM_VECTOR_LEN;
+                    return ptr;
+                }
+                ",
+            ))
+        })?;
+
         self.bind("__wbindgen_cb_drop", &|me| {
             me.expose_drop_ref();
             Ok(String::from(
diff --git a/src/lib.rs b/src/lib.rs
index f8a0f12f..7b2ffb94 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -303,6 +303,21 @@ impl JsValue {
     pub fn is_function(&self) -> bool {
         unsafe { __wbindgen_is_function(self.idx) == 1 }
     }
+
+    /// Get a string representation of the JavaScript object for debugging
+    #[cfg(feature = "std")]
+    fn as_debug_string(&self) -> String {
+        unsafe {
+            let mut len = 0;
+            let ptr = __wbindgen_debug_string(self.idx, &mut len);
+            if ptr.is_null() {
+                unreachable!("`__wbindgen_debug_string` must return a valid string")
+            } else {
+                let data = Vec::from_raw_parts(ptr, len, len);
+                String::from_utf8_unchecked(data)
+            }
+        }
+    }
 }
 
 impl PartialEq for JsValue {
@@ -477,6 +492,7 @@ externs! {
     fn __wbindgen_is_function(idx: u32) -> u32;
     fn __wbindgen_is_string(idx: u32) -> u32;
     fn __wbindgen_string_get(idx: u32, len: *mut usize) -> *mut u8;
+    fn __wbindgen_debug_string(idx: u32, len: *mut usize) -> *mut u8;
     fn __wbindgen_throw(a: *const u8, b: usize) -> !;
     fn __wbindgen_rethrow(a: u32) -> !;
 
@@ -503,30 +519,17 @@ impl Clone for JsValue {
     }
 }
 
+#[cfg(feature = "std")]
 impl fmt::Debug for JsValue {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        if let Some(n) = self.as_f64() {
-            return n.fmt(f);
-        }
-        #[cfg(feature = "std")]
-        {
-            if let Some(n) = self.as_string() {
-                return n.fmt(f);
-            }
-        }
-        if let Some(n) = self.as_bool() {
-            return n.fmt(f);
-        }
-        if self.is_null() {
-            return fmt::Display::fmt("null", f);
-        }
-        if self.is_undefined() {
-            return fmt::Display::fmt("undefined", f);
-        }
-        if self.is_symbol() {
-            return fmt::Display::fmt("Symbol(..)", f);
-        }
-        fmt::Display::fmt("[object]", f)
+        write!(f, "JsValue({})", self.as_debug_string())
+    }
+}
+
+#[cfg(not(feature = "std"))]
+impl fmt::Debug for JsValue {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.write_str("JsValue")
     }
 }
 
diff --git a/tests/wasm/api.js b/tests/wasm/api.js
index 7136b5db..4c2dec0e 100644
--- a/tests/wasm/api.js
+++ b/tests/wasm/api.js
@@ -41,3 +41,17 @@ exports.js_eq_works = () => {
     assert.strictEqual(wasm.eq_test(x, x), true);
     assert.strictEqual(wasm.eq_test1(x), true);
 };
+
+exports.debug_values = () => ([
+    null,
+    undefined,
+    0,
+    1.0,
+    true,
+    [1,2,3],
+    "string",
+    {test: "object"},
+    [1.0, [2.0, 3.0]],
+    () => (null),
+    new Set(),
+]);
diff --git a/tests/wasm/api.rs b/tests/wasm/api.rs
index e7d95133..b7263515 100644
--- a/tests/wasm/api.rs
+++ b/tests/wasm/api.rs
@@ -8,6 +8,7 @@ extern "C" {
     fn js_works();
     fn js_eq_works();
     fn assert_null(v: JsValue);
+    fn debug_values() -> JsValue;
 }
 
 #[wasm_bindgen_test]
@@ -71,7 +72,7 @@ pub fn api_get_false() -> JsValue {
 #[wasm_bindgen]
 pub fn api_test_bool(a: &JsValue, b: &JsValue, c: &JsValue) {
     assert_eq!(a.as_bool(), Some(true));
-    assert_eq!(format!("{:?}", a), "true");
+    assert_eq!(format!("{:?}", a), "JsValue(true)");
     assert_eq!(b.as_bool(), Some(false));
     assert_eq!(c.as_bool(), None);
 }
@@ -80,7 +81,7 @@ pub fn api_test_bool(a: &JsValue, b: &JsValue, c: &JsValue) {
 pub fn api_mk_symbol() -> JsValue {
     let a = JsValue::symbol(None);
     assert!(a.is_symbol());
-    assert_eq!(format!("{:?}", a), "Symbol(..)");
+    assert_eq!(format!("{:?}", a), "JsValue(Symbol)");
     return a;
 }
 
@@ -100,7 +101,7 @@ pub fn api_assert_symbols(a: &JsValue, b: &JsValue) {
 #[wasm_bindgen]
 pub fn api_acquire_string(a: &JsValue, b: &JsValue) {
     assert_eq!(a.as_string().unwrap(), "foo");
-    assert_eq!(format!("{:?}", a), "\"foo\"");
+    assert_eq!(format!("{:?}", a), "JsValue(\"foo\")");
     assert_eq!(b.as_string(), None);
 }
 
@@ -145,3 +146,24 @@ fn memory_accessor_appears_to_work() {
         .for_each(&mut |val, _, _| v.push(val));
     assert_eq!(v, [3, 0, 0, 0]);
 }
+
+#[wasm_bindgen_test]
+fn debug_output() {
+    let test_iter = debug_values().dyn_into::<js_sys::Array>().unwrap().values().into_iter();
+    let expecteds = vec![
+        "JsValue(null)",
+        "JsValue(undefined)",
+        "JsValue(0)",
+        "JsValue(1)",
+        "JsValue(true)",
+        "JsValue([1, 2, 3])",
+        "JsValue(\"string\")",
+        "JsValue(Object({\"test\":\"object\"}))",
+        "JsValue([1, [2, 3]])",
+        "JsValue(Function)",
+        "JsValue(Set)",
+    ];
+    for (test, expected) in test_iter.zip(expecteds) {
+        assert_eq!(format!("{:?}", test.unwrap()), expected);
+    }
+}