diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift
index 8136f345e..07937b01a 100644
--- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift
+++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift
@@ -804,4 +804,28 @@ try test("Hashable Conformance") {
     try expectEqual(firstHash, secondHash)
 }
 
+try test("Symbols") {
+    let symbol1 = JSSymbol("abc")
+    let symbol2 = JSSymbol("abc")
+    try expectNotEqual(symbol1, symbol2)
+    try expectEqual(symbol1.name, symbol2.name)
+    try expectEqual(symbol1.name, "abc")
+
+    try expectEqual(JSSymbol.iterator, JSSymbol.iterator)
+
+    // let hasInstanceClass = {
+    //   prop: Object.assign(function () {}, {
+    //     [Symbol.hasInstance]() { return true }
+    //   })
+    // }.prop
+    let hasInstanceObject = JSObject.global.Object.function!.new()
+    hasInstanceObject.prop = JSClosure { _ in .undefined }.jsValue()
+    let hasInstanceClass = hasInstanceObject.prop.function!
+    hasInstanceClass[JSSymbol.hasInstance] = JSClosure { _ in
+        return .boolean(true)
+    }.jsValue()
+    try expectEqual(hasInstanceClass[JSSymbol.hasInstance].function!().boolean, true)
+    try expectEqual(JSObject.global.Object.isInstanceOf(hasInstanceClass), true)
+}
+
 Expectation.wait(expectations)
diff --git a/Runtime/src/js-value.ts b/Runtime/src/js-value.ts
index 61f7b486a..67ac5d46a 100644
--- a/Runtime/src/js-value.ts
+++ b/Runtime/src/js-value.ts
@@ -10,6 +10,7 @@ export enum Kind {
     Null = 4,
     Undefined = 5,
     Function = 6,
+    Symbol = 7,
 }
 
 export const decode = (
@@ -102,6 +103,11 @@ export const write = (
             memory.writeUint32(payload1_ptr, memory.retain(value));
             break;
         }
+        case "symbol": {
+            memory.writeUint32(kind_ptr, exceptionBit | Kind.Symbol);
+            memory.writeUint32(payload1_ptr, memory.retain(value));
+            break;
+        }
         default:
             throw new Error(`Type "${typeof value}" is not supported yet`);
     }
diff --git a/Runtime/src/object-heap.ts b/Runtime/src/object-heap.ts
index 61f1925fe..2f9b1fdf3 100644
--- a/Runtime/src/object-heap.ts
+++ b/Runtime/src/object-heap.ts
@@ -22,33 +22,25 @@ export class SwiftRuntimeHeap {
     }
 
     retain(value: any) {
-        const isObject = typeof value == "object";
         const entry = this._heapEntryByValue.get(value);
-        if (isObject && entry) {
+        if (entry) {
             entry.rc++;
             return entry.id;
         }
         const id = this._heapNextKey++;
         this._heapValueById.set(id, value);
-        if (isObject) {
-            this._heapEntryByValue.set(value, { id: id, rc: 1 });
-        }
+        this._heapEntryByValue.set(value, { id: id, rc: 1 });
         return id;
     }
 
     release(ref: ref) {
         const value = this._heapValueById.get(ref);
-        const isObject = typeof value == "object";
-        if (isObject) {
-            const entry = this._heapEntryByValue.get(value)!;
-            entry.rc--;
-            if (entry.rc != 0) return;
-
-            this._heapEntryByValue.delete(value);
-            this._heapValueById.delete(ref);
-        } else {
-            this._heapValueById.delete(ref);
-        }
+        const entry = this._heapEntryByValue.get(value)!;
+        entry.rc--;
+        if (entry.rc != 0) return;
+
+        this._heapEntryByValue.delete(value);
+        this._heapValueById.delete(ref);
     }
 
     referenceHeap(ref: ref) {
diff --git a/Sources/JavaScriptKit/ConvertibleToJSValue.swift b/Sources/JavaScriptKit/ConvertibleToJSValue.swift
index 7917c32cb..deb4a523c 100644
--- a/Sources/JavaScriptKit/ConvertibleToJSValue.swift
+++ b/Sources/JavaScriptKit/ConvertibleToJSValue.swift
@@ -194,6 +194,8 @@ extension RawJSValue: ConvertibleToJSValue {
             return .undefined
         case .function:
             return .function(JSFunction(id: UInt32(payload1)))
+        case .symbol:
+            return .symbol(JSSymbol(id: UInt32(payload1)))
         }
     }
 }
@@ -225,6 +227,9 @@ extension JSValue {
         case let .function(functionRef):
             kind = .function
             payload1 = JavaScriptPayload1(functionRef.id)
+        case let .symbol(symbolRef):
+            kind = .symbol
+            payload1 = JavaScriptPayload1(symbolRef.id)
         }
         let rawValue = RawJSValue(kind: kind, payload1: payload1, payload2: payload2)
         return body(rawValue)
diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift
index 0d3a917c0..6a05a8de2 100644
--- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift
+++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift
@@ -11,7 +11,6 @@ import _CJavaScriptKit
 /// ```
 ///
 public class JSFunction: JSObject {
-
     /// Call this function with given `arguments` and binding given `this` as context.
     /// - Parameters:
     ///   - this: The value to be passed as the `this` parameter to this function.
@@ -19,7 +18,7 @@ public class JSFunction: JSObject {
     /// - Returns: The result of this call.
     @discardableResult
     public func callAsFunction(this: JSObject? = nil, arguments: [ConvertibleToJSValue]) -> JSValue {
-        invokeNonThrowingJSFunction(self, arguments: arguments, this: this)
+        invokeNonThrowingJSFunction(self, arguments: arguments, this: this).jsValue()
     }
 
     /// A variadic arguments version of `callAsFunction`.
@@ -41,7 +40,7 @@ public class JSFunction: JSObject {
     public func new(arguments: [ConvertibleToJSValue]) -> JSObject {
         arguments.withRawJSValues { rawValues in
             rawValues.withUnsafeBufferPointer { bufferPointer in
-                return JSObject(id: _call_new(self.id, bufferPointer.baseAddress!, Int32(bufferPointer.count)))
+                JSObject(id: _call_new(self.id, bufferPointer.baseAddress!, Int32(bufferPointer.count)))
             }
         }
     }
@@ -75,7 +74,7 @@ public class JSFunction: JSObject {
         fatalError("unavailable")
     }
 
-    public override class func construct(from value: JSValue) -> Self? {
+    override public class func construct(from value: JSValue) -> Self? {
         return value.function as? Self
     }
 
@@ -84,9 +83,9 @@ public class JSFunction: JSObject {
     }
 }
 
-private func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) -> JSValue {
+func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) -> RawJSValue {
     arguments.withRawJSValues { rawValues in
-        rawValues.withUnsafeBufferPointer { bufferPointer -> (JSValue) in
+        rawValues.withUnsafeBufferPointer { bufferPointer in
             let argv = bufferPointer.baseAddress
             let argc = bufferPointer.count
             var kindAndFlags = JavaScriptValueKindAndFlags()
@@ -94,8 +93,8 @@ private func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [Conve
             var payload2 = JavaScriptPayload2()
             if let thisId = this?.id {
                 _call_function_with_this_no_catch(thisId,
-                                         jsFunc.id, argv, Int32(argc),
-                                         &kindAndFlags, &payload1, &payload2)
+                                                  jsFunc.id, argv, Int32(argc),
+                                                  &kindAndFlags, &payload1, &payload2)
             } else {
                 _call_function_no_catch(
                     jsFunc.id, argv, Int32(argc),
@@ -104,7 +103,7 @@ private func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [Conve
             }
             assert(!kindAndFlags.isException)
             let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2)
-            return result.jsValue()
+            return result
         }
     }
 }
diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift
index 3bafe60b5..0768817e2 100644
--- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift
+++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift
@@ -77,6 +77,14 @@ public class JSObject: Equatable {
         set { setJSValue(this: self, index: Int32(index), value: newValue) }
     }
 
+    /// Access the `symbol` member dynamically through JavaScript and Swift runtime bridge library.
+    /// - Parameter symbol: The name of this object's member to access.
+    /// - Returns: The value of the `name` member of this object.
+    public subscript(_ name: JSSymbol) -> JSValue {
+        get { getJSValue(this: self, symbol: name) }
+        set { setJSValue(this: self, symbol: name, value: newValue) }
+    }
+
     /// A modifier to call methods as throwing methods capturing `this`
     ///
     ///
diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift
new file mode 100644
index 000000000..3ec1b2902
--- /dev/null
+++ b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift
@@ -0,0 +1,56 @@
+import _CJavaScriptKit
+
+private let Symbol = JSObject.global.Symbol.function!
+
+public class JSSymbol: JSObject {
+    public var name: String? { self["description"].string }
+
+    public init(_ description: JSString) {
+        // can’t do `self =` so we have to get the ID manually
+        let result = invokeNonThrowingJSFunction(Symbol, arguments: [description], this: nil)
+        precondition(result.kind == .symbol)
+        super.init(id: UInt32(result.payload1))
+    }
+    @_disfavoredOverload
+    public convenience init(_ description: String) {
+        self.init(JSString(description))
+    }
+
+    override init(id: JavaScriptObjectRef) {
+        super.init(id: id)
+    }
+
+    public static func `for`(key: JSString) -> JSSymbol {
+        Symbol.for!(key).symbol!
+    }
+
+    @_disfavoredOverload
+    public static func `for`(key: String) -> JSSymbol {
+        Symbol.for!(key).symbol!
+    }
+
+    public static func key(for symbol: JSSymbol) -> JSString? {
+        Symbol.keyFor!(symbol).jsString
+    }
+
+    @_disfavoredOverload
+    public static func key(for symbol: JSSymbol) -> String? {
+        Symbol.keyFor!(symbol).string
+    }
+}
+
+extension JSSymbol {
+    public static let asyncIterator: JSSymbol! = Symbol.asyncIterator.symbol
+    public static let hasInstance: JSSymbol! = Symbol.hasInstance.symbol
+    public static let isConcatSpreadable: JSSymbol! = Symbol.isConcatSpreadable.symbol
+    public static let iterator: JSSymbol! = Symbol.iterator.symbol
+    public static let match: JSSymbol! = Symbol.match.symbol
+    public static let matchAll: JSSymbol! = Symbol.matchAll.symbol
+    public static let replace: JSSymbol! = Symbol.replace.symbol
+    public static let search: JSSymbol! = Symbol.search.symbol
+    public static let species: JSSymbol! = Symbol.species.symbol
+    public static let split: JSSymbol! = Symbol.split.symbol
+    public static let toPrimitive: JSSymbol! = Symbol.toPrimitive.symbol
+    public static let toStringTag: JSSymbol! = Symbol.toStringTag.symbol
+    public static let unscopables: JSSymbol! = Symbol.unscopables.symbol
+}
diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift
index 34bb78232..b363db679 100644
--- a/Sources/JavaScriptKit/JSValue.swift
+++ b/Sources/JavaScriptKit/JSValue.swift
@@ -10,6 +10,7 @@ public enum JSValue: Equatable {
     case null
     case undefined
     case function(JSFunction)
+    case symbol(JSSymbol)
 
     /// Returns the `Bool` value of this JS value if its type is boolean.
     /// If not, returns `nil`.
@@ -67,6 +68,13 @@ public enum JSValue: Equatable {
         }
     }
 
+    public var symbol: JSSymbol? {
+        switch self {
+        case let .symbol(symbol): return symbol
+        default: return nil
+        }
+    }
+
     /// Returns the `true` if this JS value is null.
     /// If not, returns `false`.
     public var isNull: Bool {
@@ -80,23 +88,23 @@ public enum JSValue: Equatable {
     }
 }
 
-extension JSValue {
+public extension JSValue {
     /// An unsafe convenience method of `JSObject.subscript(_ name: String) -> ((ConvertibleToJSValue...) -> JSValue)?`
     /// - Precondition: `self` must be a JavaScript Object and specified member should be a callable object.
-    public subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) -> JSValue) {
+    subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) -> JSValue) {
         object![dynamicMember: name]!
     }
 
     /// An unsafe convenience method of `JSObject.subscript(_ index: Int) -> JSValue`
     /// - Precondition: `self` must be a JavaScript Object.
-    public subscript(dynamicMember name: String) -> JSValue {
+    subscript(dynamicMember name: String) -> JSValue {
         get { self.object![name] }
         set { self.object![name] = newValue }
     }
 
     /// An unsafe convenience method of `JSObject.subscript(_ index: Int) -> JSValue`
     /// - Precondition: `self` must be a JavaScript Object.
-    public subscript(_ index: Int) -> JSValue {
+    subscript(_ index: Int) -> JSValue {
         get { object![index] }
         set { object![index] = newValue }
     }
@@ -104,15 +112,14 @@ extension JSValue {
 
 extension JSValue: Swift.Error {}
 
-extension JSValue {
-    public func fromJSValue<Type>() -> Type? where Type: ConstructibleFromJSValue {
+public extension JSValue {
+    func fromJSValue<Type>() -> Type? where Type: ConstructibleFromJSValue {
         return Type.construct(from: self)
     }
 }
 
-extension JSValue {
-
-    public static func string(_ value: String) -> JSValue {
+public extension JSValue {
+    static func string(_ value: String) -> JSValue {
         .string(JSString(value))
     }
 
@@ -141,12 +148,12 @@ extension JSValue {
     /// eventListenter.release()
     /// ```
     @available(*, deprecated, message: "Please create JSClosure directly and manage its lifetime manually.")
-    public static func function(_ body: @escaping ([JSValue]) -> JSValue) -> JSValue {
+    static func function(_ body: @escaping ([JSValue]) -> JSValue) -> JSValue {
         .object(JSClosure(body))
     }
 
     @available(*, deprecated, renamed: "object", message: "JSClosure is no longer a subclass of JSFunction. Use .object(closure) instead.")
-    public static func function(_ closure: JSClosure) -> JSValue {
+    static func function(_ closure: JSClosure) -> JSValue {
         .object(closure)
     }
 }
@@ -170,7 +177,7 @@ extension JSValue: ExpressibleByFloatLiteral {
 }
 
 extension JSValue: ExpressibleByNilLiteral {
-    public init(nilLiteral: ()) {
+    public init(nilLiteral _: ()) {
         self = .null
     }
 }
@@ -205,14 +212,28 @@ public func setJSValue(this: JSObject, index: Int32, value: JSValue) {
     }
 }
 
-extension JSValue {
-  /// Return `true` if this value is an instance of the passed `constructor` function.
-  /// Returns `false` for everything except objects and functions.
-  /// - Parameter constructor: The constructor function to check.
-  /// - Returns: The result of `instanceof` in the JavaScript environment.
-    public func isInstanceOf(_ constructor: JSFunction) -> Bool {
+public func getJSValue(this: JSObject, symbol: JSSymbol) -> JSValue {
+    var rawValue = RawJSValue()
+    _get_prop(this.id, symbol.id,
+              &rawValue.kind,
+              &rawValue.payload1, &rawValue.payload2)
+    return rawValue.jsValue()
+}
+
+public func setJSValue(this: JSObject, symbol: JSSymbol, value: JSValue) {
+    value.withRawJSValue { rawValue in
+        _set_prop(this.id, symbol.id, rawValue.kind, rawValue.payload1, rawValue.payload2)
+    }
+}
+
+public extension JSValue {
+    /// Return `true` if this value is an instance of the passed `constructor` function.
+    /// Returns `false` for everything except objects and functions.
+    /// - Parameter constructor: The constructor function to check.
+    /// - Returns: The result of `instanceof` in the JavaScript environment.
+    func isInstanceOf(_ constructor: JSFunction) -> Bool {
         switch self {
-        case .boolean, .string, .number, .null, .undefined:
+        case .boolean, .string, .number, .null, .undefined, .symbol:
             return false
         case let .object(ref):
             return ref.isInstanceOf(constructor)
@@ -227,11 +248,12 @@ extension JSValue: CustomStringConvertible {
         switch self {
         case let .boolean(boolean):
             return boolean.description
-        case .string(let string):
+        case let .string(string):
             return string.description
-        case .number(let number):
+        case let .number(number):
             return number.description
-        case .object(let object), .function(let object as JSObject):
+        case let .object(object), let .function(object as JSObject),
+             .symbol(let object as JSObject):
             return object.toString!().fromJSValue()!
         case .null:
             return "null"
diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h
index ce0bf5862..daf405141 100644
--- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h
+++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h
@@ -21,6 +21,7 @@ typedef enum __attribute__((enum_extensibility(closed))) {
   JavaScriptValueKindNull = 4,
   JavaScriptValueKindUndefined = 5,
   JavaScriptValueKindFunction = 6,
+  JavaScriptValueKindSymbol = 7,
 } JavaScriptValueKind;
 
 typedef struct {
@@ -60,6 +61,10 @@ typedef double JavaScriptPayload2;
 ///    payload1: the target `JavaScriptHostFuncRef`
 ///    payload2: 0
 ///
+/// For symbol value:
+///    payload1: `JavaScriptObjectRef`
+///    payload2: 0
+///
 typedef struct {
   JavaScriptValueKind kind;
   JavaScriptPayload1 payload1;