diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 79e1ca3..fdd061f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,10 +7,12 @@ on:
 jobs:
   PHPUnit:
     name: PHPUnit (PHP ${{ matrix.php }})
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-22.04
     strategy:
       matrix:
         php:
+          - 8.3
+          - 8.2
           - 8.1
           - 8.0
           - 7.4
@@ -23,7 +25,7 @@ jobs:
           - 5.4
           - 5.3
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v4
       - uses: shivammathur/setup-php@v2
         with:
           php-version: ${{ matrix.php }}
@@ -37,14 +39,21 @@ jobs:
 
   PHPUnit-hhvm:
     name: PHPUnit (HHVM)
-    runs-on: ubuntu-18.04
+    runs-on: ubuntu-22.04
     continue-on-error: true
+    services:
+      redis:
+        image: redis
     steps:
-      - uses: actions/checkout@v2
-      - uses: azjezz/setup-hhvm@v1
+      - uses: actions/checkout@v4
+      - run: cp "$(which composer)" composer.phar && ./composer.phar self-update --2.2 # downgrade Composer for HHVM
+      - name: Run hhvm composer.phar install
+        uses: docker://hhvm/hhvm:3.30-lts-latest
         with:
-          version: lts-3.30
-      - run: composer self-update --2.2 # downgrade Composer for HHVM
-      - run: hhvm $(which composer) install
-      - run: docker run --net=host -d redis
-      - run: REDIS_URI=localhost:6379 hhvm vendor/bin/phpunit
+          args: hhvm composer.phar install
+      - name: Run REDIS_URI=redis:6379 hhvm vendor/bin/phpunit
+        uses: docker://hhvm/hhvm:3.30-lts-latest
+        with:
+          args: hhvm vendor/bin/phpunit
+        env:
+          REDIS_URI: redis:6379
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c5e8e66..4ef05f9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
 # Changelog
 
+## 2.7.0 (2024-01-05)
+
+This is a compatibility release that contains backported features from the v3 branch.
+Once v3 is released, it will be the way forward for this project.
+
+*   Feature: Forward compatibility with Promise v3.
+    (#152 by @clue)
+
+*   Feature: Full PHP 8.3 compatibility and update test suite.
+    (#151 by @clue)
+
 ## 2.6.0 (2022-05-09)
 
 *   Feature: Support PHP 8.1 release.
diff --git a/README.md b/README.md
index 5492572..c15fbee 100644
--- a/README.md
+++ b/README.md
@@ -610,7 +610,7 @@ This project follows [SemVer](https://semver.org/).
 This will install the latest supported version:
 
 ```bash
-$ composer require clue/redis-react:^2.6
+$ composer require clue/redis-react:^2.7
 ```
 
 See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
diff --git a/composer.json b/composer.json
index c1752cc..82dba29 100644
--- a/composer.json
+++ b/composer.json
@@ -15,18 +15,22 @@
         "clue/redis-protocol": "0.3.*",
         "evenement/evenement": "^3.0 || ^2.0 || ^1.0",
         "react/event-loop": "^1.2",
-        "react/promise": "^2.0 || ^1.1",
-        "react/promise-timer": "^1.8",
-        "react/socket": "^1.9"
+        "react/promise": "^3 || ^2.0 || ^1.1",
+        "react/promise-timer": "^1.9",
+        "react/socket": "^1.12"
     },
     "require-dev": {
-        "clue/block-react": "^1.1",
-        "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35"
+        "clue/block-react": "^1.5",
+        "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
     },
     "autoload": {
-        "psr-4": { "Clue\\React\\Redis\\": "src/" }
+        "psr-4": {
+            "Clue\\React\\Redis\\": "src/"
+         }
     },
     "autoload-dev": {
-        "psr-4": { "Clue\\Tests\\React\\Redis\\": "tests/" }
+        "psr-4": {
+            "Clue\\Tests\\React\\Redis\\": "tests/"
+        }
     }
 }
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 5093fa5..22e4b94 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,8 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
-<!-- PHPUnit configuration file with new format for PHPUnit 9.3+ -->
-<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
-         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
+<!-- PHPUnit configuration file with new format for PHPUnit 9.6+ -->
+<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
          bootstrap="vendor/autoload.php"
          cacheResult="false"
          colors="true"
@@ -17,4 +17,7 @@
             <directory>./src/</directory>
         </include>
     </coverage>
+    <php>
+        <ini name="error_reporting" value="-1" />
+    </php>
 </phpunit>
diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy
index 8d93c4f..5e5303d 100644
--- a/phpunit.xml.legacy
+++ b/phpunit.xml.legacy
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
-<!-- PHPUnit configuration file with old format for PHPUnit 9.2 or older -->
+<!-- PHPUnit configuration file with old format for legacy PHPUnit -->
 <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/4.8/phpunit.xsd"
          bootstrap="vendor/autoload.php"
@@ -15,4 +15,7 @@
             <directory>./src/</directory>
         </whitelist>
     </filter>
+    <php>
+        <ini name="error_reporting" value="-1" />
+    </php>
 </phpunit>
diff --git a/src/Factory.php b/src/Factory.php
index 4e94905..fca8288 100644
--- a/src/Factory.php
+++ b/src/Factory.php
@@ -27,7 +27,7 @@ class Factory
      * @param ?ConnectorInterface $connector
      * @param ?ProtocolFactory $protocol
      */
-    public function __construct(LoopInterface $loop = null, ConnectorInterface $connector = null, ProtocolFactory $protocol = null)
+    public function __construct(?LoopInterface $loop = null, ?ConnectorInterface $connector = null, ?ProtocolFactory $protocol = null)
     {
         $this->loop = $loop ?: Loop::get();
         $this->connector = $connector ?: new Connector(array(), $this->loop);
@@ -84,6 +84,8 @@ public function createClient($uri)
             // either close successful connection or cancel pending connection attempt
             $connecting->then(function (ConnectionInterface $connection) {
                 $connection->close();
+            }, function () {
+                // ignore to avoid reporting unhandled rejection
             });
             $connecting->cancel();
         });
diff --git a/src/LazyClient.php b/src/LazyClient.php
index d82b257..0e42cc8 100644
--- a/src/LazyClient.php
+++ b/src/LazyClient.php
@@ -168,6 +168,8 @@ public function close()
         if ($this->promise !== null) {
             $this->promise->then(function (Client $redis) {
                 $redis->close();
+            }, function () {
+                // ignore to avoid reporting unhandled rejection
             });
             if ($this->promise !== null) {
                 $this->promise->cancel();
diff --git a/src/StreamingClient.php b/src/StreamingClient.php
index 8afd84d..7eacc5a 100644
--- a/src/StreamingClient.php
+++ b/src/StreamingClient.php
@@ -28,7 +28,7 @@ class StreamingClient extends EventEmitter implements Client
     private $subscribed = 0;
     private $psubscribed = 0;
 
-    public function __construct(DuplexStreamInterface $stream, ParserInterface $parser = null, SerializerInterface $serializer = null)
+    public function __construct(DuplexStreamInterface $stream, ?ParserInterface $parser = null, ?SerializerInterface $serializer = null)
     {
         if ($parser === null || $serializer === null) {
             $factory = new ProtocolFactory();
diff --git a/tests/FactoryLazyClientTest.php b/tests/FactoryLazyClientTest.php
index 8b5005b..fb394d1 100644
--- a/tests/FactoryLazyClientTest.php
+++ b/tests/FactoryLazyClientTest.php
@@ -34,13 +34,13 @@ public function testConstructWithoutLoopAssignsLoopAutomatically()
 
     public function testWillConnectWithDefaultPort()
     {
-        $this->connector->expects($this->never())->method('connect')->with('redis.example.com:6379')->willReturn(Promise\reject(new \RuntimeException()));
+        $this->connector->expects($this->never())->method('connect');
         $this->factory->createLazyClient('redis.example.com');
     }
 
     public function testWillConnectToLocalhost()
     {
-        $this->connector->expects($this->never())->method('connect')->with('localhost:1337')->willReturn(Promise\reject(new \RuntimeException()));
+        $this->connector->expects($this->never())->method('connect');
         $this->factory->createLazyClient('localhost:1337');
     }
 
@@ -147,7 +147,7 @@ public function testWillWriteSelectCommandIfRedisUnixUriContainsDbQueryParameter
 
     public function testWillRejectIfConnectorRejects()
     {
-        $this->connector->expects($this->never())->method('connect')->with('127.0.0.1:2')->willReturn(Promise\reject(new \RuntimeException()));
+        $this->connector->expects($this->never())->method('connect');
         $redis = $this->factory->createLazyClient('redis://127.0.0.1:2');
 
         $this->assertInstanceOf('Clue\React\Redis\Client', $redis);
diff --git a/tests/FactoryStreamingClientTest.php b/tests/FactoryStreamingClientTest.php
index 882af76..6ce03a5 100644
--- a/tests/FactoryStreamingClientTest.php
+++ b/tests/FactoryStreamingClientTest.php
@@ -44,13 +44,17 @@ public function testCtor()
     public function testWillConnectWithDefaultPort()
     {
         $this->connector->expects($this->once())->method('connect')->with('redis.example.com:6379')->willReturn(Promise\reject(new \RuntimeException()));
-        $this->factory->createClient('redis.example.com');
+        $promise = $this->factory->createClient('redis.example.com');
+
+        $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
     }
 
     public function testWillConnectToLocalhost()
     {
         $this->connector->expects($this->once())->method('connect')->with('localhost:1337')->willReturn(Promise\reject(new \RuntimeException()));
-        $this->factory->createClient('localhost:1337');
+        $promise = $this->factory->createClient('localhost:1337');
+
+        $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
     }
 
     public function testWillResolveIfConnectorResolves()
diff --git a/tests/LazyClientTest.php b/tests/LazyClientTest.php
index 2ad644e..e1af1c6 100644
--- a/tests/LazyClientTest.php
+++ b/tests/LazyClientTest.php
@@ -148,7 +148,10 @@ public function testPingAfterPreviousFactoryRejectsUnderlyingClientWillCreateNew
             new Promise(function () { })
         );
 
-        $this->redis->ping();
+        $promise = $this->redis->ping();
+
+        $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
+
         $deferred->reject($error);
 
         $this->redis->ping();
@@ -213,7 +216,7 @@ public function testPingAfterPingWillNotStartIdleTimerWhenFirstPingResolves()
 
         $this->redis->ping();
         $this->redis->ping();
-        $deferred->resolve();
+        $deferred->resolve(null);
     }
 
     public function testPingAfterPingWillStartAndCancelIdleTimerWhenSecondPingStartsAfterFirstResolves()
@@ -232,15 +235,15 @@ public function testPingAfterPingWillStartAndCancelIdleTimerWhenSecondPingStarts
         $this->loop->expects($this->once())->method('cancelTimer')->with($timer);
 
         $this->redis->ping();
-        $deferred->resolve();
+        $deferred->resolve(null);
         $this->redis->ping();
     }
 
     public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWithoutCloseEvent()
     {
         $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock();
-        $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve());
-        $client->expects($this->once())->method('close')->willReturn(\React\Promise\resolve());
+        $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve(null));
+        $client->expects($this->once())->method('close');
 
         $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client));
 
@@ -295,14 +298,17 @@ public function testCloseAfterPingWillEmitCloseWithoutErrorWhenUnderlyingClientC
         $this->redis->on('error', $this->expectCallableNever());
         $this->redis->on('close', $this->expectCallableOnce());
 
-        $this->redis->ping();
+        $promise = $this->redis->ping();
+
+        $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
+
         $this->redis->close();
     }
 
     public function testCloseAfterPingWillCloseUnderlyingClientConnectionWhenAlreadyResolved()
     {
         $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock();
-        $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve());
+        $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve(null));
         $client->expects($this->once())->method('close');
 
         $deferred = new Deferred();
@@ -327,7 +333,7 @@ public function testCloseAfterPingWillCancelIdleTimerWhenPingIsAlreadyResolved()
         $this->loop->expects($this->once())->method('cancelTimer')->with($timer);
 
         $this->redis->ping();
-        $deferred->resolve();
+        $deferred->resolve(null);
         $this->redis->close();
     }
 
@@ -404,7 +410,7 @@ public function testEmitsNoErrorEventWhenUnderlyingClientEmitsError()
         $error = new \RuntimeException();
 
         $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock();
-        $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve());
+        $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve(null));
 
         $deferred = new Deferred();
         $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise());
@@ -419,7 +425,7 @@ public function testEmitsNoErrorEventWhenUnderlyingClientEmitsError()
     public function testEmitsNoCloseEventWhenUnderlyingClientEmitsClose()
     {
         $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock();
-        $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve());
+        $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve(null));
 
         $deferred = new Deferred();
         $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise());
@@ -453,7 +459,7 @@ public function testEmitsNoCloseEventButWillCancelIdleTimerWhenUnderlyingConnect
         $this->redis->on('close', $this->expectCallableNever());
 
         $this->redis->ping();
-        $deferred->resolve();
+        $deferred->resolve(null);
 
         $this->assertTrue(is_callable($closeHandler));
         $closeHandler();
@@ -463,7 +469,7 @@ public function testEmitsMessageEventWhenUnderlyingClientEmitsMessageForPubSubCh
     {
         $messageHandler = null;
         $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock();
-        $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve());
+        $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve(null));
         $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$messageHandler) {
             if ($event === 'message') {
                 $messageHandler = $callback;
@@ -485,7 +491,7 @@ public function testEmitsUnsubscribeAndPunsubscribeEventsWhenUnderlyingClientClo
     {
         $allHandler = null;
         $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock();
-        $client->expects($this->exactly(6))->method('__call')->willReturn(\React\Promise\resolve());
+        $client->expects($this->exactly(6))->method('__call')->willReturn(\React\Promise\resolve(null));
         $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$allHandler) {
             if (!isset($allHandler[$event])) {
                 $allHandler[$event] = $callback;