diff --git a/UPGRADING b/UPGRADING
index c0f1771eb8a9..d12deec6dbf3 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -19,6 +19,16 @@ PHP 8.4 UPGRADE NOTES
 1. Backward Incompatible Changes
 ========================================
 
+- Core:
+  . isset() and empty() now check that the offset for string values is of the
+    correct type i.e. numeric, similarly to array and objects.
+    This means code similar to:
+    $a = 'string';
+    var_dump(isset($a['b']));
+    var_dump(empty($a['b']));
+    will now warn with: "Cannot access offset of type string on string"
+    when previously it silently returned false.
+
 - DOM:
   . New methods and constants were added to some DOM classes. If you inherit
     from these and you happen to have a method or property with the same name,
diff --git a/Zend/Optimizer/zend_inference.c b/Zend/Optimizer/zend_inference.c
index e971b3ba2e9f..f4e8d2114c31 100644
--- a/Zend/Optimizer/zend_inference.c
+++ b/Zend/Optimizer/zend_inference.c
@@ -5115,7 +5115,8 @@ ZEND_API bool zend_may_throw_ex(const zend_op *opline, const zend_ssa_op *ssa_op
 				return 0;
 			}
 		case ZEND_FETCH_IS:
-			return (t2 & (MAY_BE_ARRAY|MAY_BE_OBJECT));
+			return (t2 & (MAY_BE_ARRAY|MAY_BE_OBJECT))
+				|| (t1 & MAY_BE_STRING && t2 & MAY_BE_STRING);
 		case ZEND_ISSET_ISEMPTY_DIM_OBJ:
 			return (t1 & MAY_BE_OBJECT) || (t2 & (MAY_BE_ARRAY|MAY_BE_OBJECT));
 		case ZEND_FETCH_DIM_IS:
diff --git a/Zend/tests/bug60362.phpt b/Zend/tests/bug60362.phpt
deleted file mode 100644
index 51a47760a26e..000000000000
--- a/Zend/tests/bug60362.phpt
+++ /dev/null
@@ -1,75 +0,0 @@
---TEST--
-Bug #60362: non-existent sub-sub keys should not have values
---FILE--
-<?php
-$arr = array('exists' => 'foz');
-
-if (isset($arr['exists']['non_existent'])) {
-    echo "sub-key 'non_existent' is set: ";
-    var_dump($arr['exists']['non_existent']);
-} else {
-    echo "sub-key 'non_existent' is not set.\n";
-}
-if (isset($arr['exists'][1])) {
-    echo "sub-key 1 is set: ";
-    var_dump($arr['exists'][1]);
-} else {
-    echo "sub-key 1 is not set.\n";
-}
-
-echo "-------------------\n";
-if (isset($arr['exists']['non_existent']['sub_sub'])) {
-    echo "sub-key 'sub_sub' is set: ";
-    var_dump($arr['exists']['non_existent']['sub_sub']);
-} else {
-    echo "sub-sub-key 'sub_sub' is not set.\n";
-}
-if (isset($arr['exists'][1][0])) {
-    echo "sub-sub-key 0 is set: ";
-    var_dump($arr['exists'][1][0]);
-} else {
-    echo "sub-sub-key 0 is not set.\n";
-}
-
-echo "-------------------\n";
-if (empty($arr['exists']['non_existent'])) {
-    echo "sub-key 'non_existent' is empty.\n";
-} else {
-    echo "sub-key 'non_existent' is not empty: ";
-    var_dump($arr['exists']['non_existent']);
-}
-if (empty($arr['exists'][1])) {
-    echo "sub-key 1 is empty.\n";
-} else {
-    echo "sub-key 1 is not empty: ";
-    var_dump($arr['exists'][1]);
-}
-
-echo "-------------------\n";
-if (empty($arr['exists']['non_existent']['sub_sub'])) {
-    echo "sub-sub-key 'sub_sub' is empty.\n";
-} else {
-    echo "sub-sub-key 'sub_sub' is not empty: ";
-    var_dump($arr['exists']['non_existent']['sub_sub']);
-}
-if (empty($arr['exists'][1][0])) {
-    echo "sub-sub-key 0 is empty.\n";
-} else {
-    echo "sub-sub-key 0 is not empty: ";
-    var_dump($arr['exists'][1][0]);
-}
-echo "DONE";
-?>
---EXPECT--
-sub-key 'non_existent' is not set.
-sub-key 1 is set: string(1) "o"
--------------------
-sub-sub-key 'sub_sub' is not set.
-sub-sub-key 0 is set: string(1) "o"
--------------------
-sub-key 'non_existent' is empty.
-sub-key 1 is not empty: string(1) "o"
--------------------
-sub-sub-key 'sub_sub' is empty.
-sub-sub-key 0 is not empty: string(1) "o"
-DONE
diff --git a/Zend/tests/bug62680.phpt b/Zend/tests/bug62680.phpt
deleted file mode 100644
index e2a2366e7a92..000000000000
--- a/Zend/tests/bug62680.phpt
+++ /dev/null
@@ -1,11 +0,0 @@
---TEST--
-Bug #62680 (Function isset() throws fatal error on set array if non-existent key depth >= 3)
---FILE--
-<?php
-$array = array("");
-var_dump(isset($array[0]["a"]["b"]));
-var_dump(isset($array[0]["a"]["b"]["c"]));
-?>
---EXPECT--
-bool(false)
-bool(false)
diff --git a/Zend/tests/bug69889.phpt b/Zend/tests/bug69889.phpt
deleted file mode 100644
index dd555ab407a4..000000000000
--- a/Zend/tests/bug69889.phpt
+++ /dev/null
@@ -1,21 +0,0 @@
---TEST--
-Bug #69889: Null coalesce operator doesn't work for string offsets
---FILE--
-<?php
-
-$foo = "test";
-var_dump($foo[0] ?? "default");
-
-var_dump($foo[5] ?? "default");
-var_dump(isset($foo[5]) ? $foo[5] : "default");
-
-var_dump($foo["str"] ?? "default");
-var_dump(isset($foo["str"]) ? $foo["str"] : "default");
-
-?>
---EXPECT--
-string(1) "t"
-string(7) "default"
-string(7) "default"
-string(7) "default"
-string(7) "default"
diff --git a/Zend/tests/empty_str_offset.phpt b/Zend/tests/empty_str_offset.phpt
deleted file mode 100644
index 268c0d4869b6..000000000000
--- a/Zend/tests/empty_str_offset.phpt
+++ /dev/null
@@ -1,135 +0,0 @@
---TEST--
-Testing empty() with string offsets
---FILE--
-<?php
-
-print "- empty ---\n";
-
-$str = "test0123";
-
-var_dump(empty($str[-1]));
-var_dump(empty($str[-10]));
-var_dump(empty($str[-4])); // 0
-var_dump(empty($str[0]));
-var_dump(empty($str[1]));
-var_dump(empty($str[4])); // 0
-var_dump(empty($str[5])); // 1
-var_dump(empty($str[8]));
-var_dump(empty($str[10000]));
-// non-numeric offsets
-print "- string literal ---\n";
-var_dump(empty($str['-1'])); // 3
-var_dump(empty($str['-10']));
-var_dump(empty($str['0']));
-var_dump(empty($str['1']));
-var_dump(empty($str['4'])); // 0
-var_dump(empty($str['1.5']));
-var_dump(empty($str['good']));
-var_dump(empty($str['3 and a half']));
-print "- string variable ---\n";
-var_dump(empty($str[$key = '-1'])); // 3
-var_dump(empty($str[$key = '-10']));
-var_dump(empty($str[$key = '0']));
-var_dump(empty($str[$key = '1']));
-var_dump(empty($str[$key = '4'])); // 0
-var_dump(empty($str[$key = '1.5']));
-var_dump(empty($str[$key = 'good']));
-var_dump(empty($str[$key = '3 and a half']));
-print "- bool ---\n";
-var_dump(empty($str[true]));
-var_dump(empty($str[false]));
-var_dump(empty($str[false][true]));
-print "- null ---\n";
-var_dump(empty($str[null]));
-print "- double ---\n";
-var_dump(empty($str[-1.1]));
-var_dump(empty($str[-10.5]));
-var_dump(empty($str[-4.1]));
-var_dump(empty($str[-0.8]));
-var_dump(empty($str[-0.1]));
-var_dump(empty($str[0.2]));
-var_dump(empty($str[0.9]));
-var_dump(empty($str[M_PI]));
-var_dump(empty($str[100.5001]));
-print "- array ---\n";
-var_dump(empty($str[array()]));
-var_dump(empty($str[array(1,2,3)]));
-print "- object ---\n";
-var_dump(empty($str[new stdClass()]));
-print "- resource ---\n";
-$f = fopen(__FILE__, 'r');
-var_dump(empty($str[$f]));
-print "done\n";
-
-?>
---EXPECTF--
-- empty ---
-bool(false)
-bool(true)
-bool(true)
-bool(false)
-bool(false)
-bool(true)
-bool(false)
-bool(true)
-bool(true)
-- string literal ---
-bool(false)
-bool(true)
-bool(false)
-bool(false)
-bool(true)
-bool(true)
-bool(true)
-bool(true)
-- string variable ---
-bool(false)
-bool(true)
-bool(false)
-bool(false)
-bool(true)
-bool(true)
-bool(true)
-bool(true)
-- bool ---
-bool(false)
-bool(false)
-bool(true)
-- null ---
-bool(false)
-- double ---
-
-Deprecated: Implicit conversion from float -1.1 to int loses precision in %s on line %d
-bool(false)
-
-Deprecated: Implicit conversion from float -10.5 to int loses precision in %s on line %d
-bool(true)
-
-Deprecated: Implicit conversion from float -4.1 to int loses precision in %s on line %d
-bool(true)
-
-Deprecated: Implicit conversion from float -0.8 to int loses precision in %s on line %d
-bool(false)
-
-Deprecated: Implicit conversion from float -0.1 to int loses precision in %s on line %d
-bool(false)
-
-Deprecated: Implicit conversion from float 0.2 to int loses precision in %s on line %d
-bool(false)
-
-Deprecated: Implicit conversion from float 0.9 to int loses precision in %s on line %d
-bool(false)
-
-Deprecated: Implicit conversion from float 3.141592653589793 to int loses precision in %s on line %d
-bool(false)
-
-Deprecated: Implicit conversion from float 100.5001 to int loses precision in %s on line %d
-bool(true)
-- array ---
-bool(true)
-bool(true)
-- object ---
-bool(true)
-- resource ---
-bool(true)
-done
diff --git a/Zend/tests/isset_str_offset.phpt b/Zend/tests/isset_str_offset.phpt
deleted file mode 100644
index f819c9dfe139..000000000000
--- a/Zend/tests/isset_str_offset.phpt
+++ /dev/null
@@ -1,129 +0,0 @@
---TEST--
-Testing isset with string offsets
---FILE--
-<?php
-
-print "- isset ---\n";
-
-$str = "test0123";
-
-var_dump(isset($str[-1]));
-var_dump(isset($str[-10]));
-var_dump(isset($str[0]));
-var_dump(isset($str[1]));
-var_dump(isset($str[4])); // 0
-var_dump(isset($str[5])); // 1
-var_dump(isset($str[8]));
-var_dump(isset($str[10000]));
-// non-numeric offsets
-print "- string literal ---\n";
-var_dump(isset($str['-1'])); // 3
-var_dump(isset($str['-10']));
-var_dump(isset($str['0']));
-var_dump(isset($str['1']));
-var_dump(isset($str['4'])); // 0
-var_dump(isset($str['1.5']));
-var_dump(isset($str['good']));
-var_dump(isset($str['3 and a half']));
-print "- string variable ---\n";
-var_dump(isset($str[$key = '-1'])); // 3
-var_dump(isset($str[$key = '-10']));
-var_dump(isset($str[$key = '0']));
-var_dump(isset($str[$key = '1']));
-var_dump(isset($str[$key = '4'])); // 0
-var_dump(isset($str[$key = '1.5']));
-var_dump(isset($str[$key = 'good']));
-var_dump(isset($str[$key = '3 and a half']));
-print "- bool ---\n";
-var_dump(isset($str[true]));
-var_dump(isset($str[false]));
-var_dump(isset($str[false][true]));
-print "- null ---\n";
-var_dump(isset($str[null]));
-print "- double ---\n";
-var_dump(isset($str[-1.1]));
-var_dump(isset($str[-10.5]));
-var_dump(isset($str[-0.8]));
-var_dump(isset($str[-0.1]));
-var_dump(isset($str[0.2]));
-var_dump(isset($str[0.9]));
-var_dump(isset($str[M_PI]));
-var_dump(isset($str[100.5001]));
-print "- array ---\n";
-var_dump(isset($str[array()]));
-var_dump(isset($str[array(1,2,3)]));
-print "- object ---\n";
-var_dump(isset($str[new stdClass()]));
-print "- resource ---\n";
-$f = fopen(__FILE__, 'r');
-var_dump(isset($str[$f]));
-print "done\n";
-
-?>
---EXPECTF--
-- isset ---
-bool(true)
-bool(false)
-bool(true)
-bool(true)
-bool(true)
-bool(true)
-bool(false)
-bool(false)
-- string literal ---
-bool(true)
-bool(false)
-bool(true)
-bool(true)
-bool(true)
-bool(false)
-bool(false)
-bool(false)
-- string variable ---
-bool(true)
-bool(false)
-bool(true)
-bool(true)
-bool(true)
-bool(false)
-bool(false)
-bool(false)
-- bool ---
-bool(true)
-bool(true)
-bool(false)
-- null ---
-bool(true)
-- double ---
-
-Deprecated: Implicit conversion from float -1.1 to int loses precision in %s on line %d
-bool(true)
-
-Deprecated: Implicit conversion from float -10.5 to int loses precision in %s on line %d
-bool(false)
-
-Deprecated: Implicit conversion from float -0.8 to int loses precision in %s on line %d
-bool(true)
-
-Deprecated: Implicit conversion from float -0.1 to int loses precision in %s on line %d
-bool(true)
-
-Deprecated: Implicit conversion from float 0.2 to int loses precision in %s on line %d
-bool(true)
-
-Deprecated: Implicit conversion from float 0.9 to int loses precision in %s on line %d
-bool(true)
-
-Deprecated: Implicit conversion from float 3.141592653589793 to int loses precision in %s on line %d
-bool(true)
-
-Deprecated: Implicit conversion from float 100.5001 to int loses precision in %s on line %d
-bool(false)
-- array ---
-bool(false)
-bool(false)
-- object ---
-bool(false)
-- resource ---
-bool(false)
-done
diff --git a/Zend/tests/offsets_isset_empty/array_isset_empty_coalese_errors.phpt b/Zend/tests/offsets_isset_empty/array_isset_empty_coalese_errors.phpt
new file mode 100644
index 000000000000..bda26329a3f6
--- /dev/null
+++ b/Zend/tests/offsets_isset_empty/array_isset_empty_coalese_errors.phpt
@@ -0,0 +1,100 @@
+--TEST--
+Invalid array offset types throws TypeError on isset/empty/coalese
+--FILE--
+<?php
+
+$v = ['Hello', 'world'];
+$o = new stdClass();
+$a = [];
+$r = STDERR;
+
+echo "Objects as offsets:\n";
+try {
+    var_dump(isset($v[$o]));
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+try {
+    var_dump(empty($v[$o]));
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+try {
+    var_dump($v[$o] ?? 'default');
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+echo 'Array ($variable) as offsets:', "\n";
+try {
+    var_dump(isset($v[$a]));
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+try {
+    var_dump(empty($v[$a]));
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+try {
+    var_dump($v[$a] ?? 'default');
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+echo "Array (constant empty array) as offsets:\n";
+try {
+    var_dump(isset($v[[]]));
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+try {
+    var_dump(empty($v[[]]));
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+try {
+    var_dump($v[[]] ?? 'default');
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+echo "Resource as offsets:\n";
+try {
+    var_dump(isset($v[$r]));
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+try {
+    var_dump(empty($v[$r]));
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+try {
+    var_dump($v[$r] ?? 'default');
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+Objects as offsets:
+Cannot access offset of type stdClass in isset or empty
+Cannot access offset of type stdClass in isset or empty
+Cannot access offset of type stdClass on array
+Array ($variable) as offsets:
+Cannot access offset of type array in isset or empty
+Cannot access offset of type array in isset or empty
+Cannot access offset of type array on array
+Array (constant empty array) as offsets:
+Cannot access offset of type array in isset or empty
+Cannot access offset of type array in isset or empty
+Cannot access offset of type array on array
+Resource as offsets:
+
+Warning: Resource ID#3 used as offset, casting to integer (3) in %s on line %d
+bool(false)
+
+Warning: Resource ID#3 used as offset, casting to integer (3) in %s on line %d
+bool(true)
+
+Warning: Resource ID#3 used as offset, casting to integer (3) in %s on line %d
+string(7) "default"
+
diff --git a/Zend/tests/bug31098.phpt b/Zend/tests/offsets_isset_empty/bug31098.phpt
similarity index 69%
rename from Zend/tests/bug31098.phpt
rename to Zend/tests/offsets_isset_empty/bug31098.phpt
index 862fc6fc46dc..cb25cd6b41fe 100644
--- a/Zend/tests/bug31098.phpt
+++ b/Zend/tests/offsets_isset_empty/bug31098.phpt
@@ -9,11 +9,26 @@ var_dump(isset($a->b));
 $a = '0';
 var_dump(isset($a->b));
 $a = '';
-var_dump(isset($a['b']));
+
+try {
+    var_dump(isset($a['b']));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
 $a = 'a';
-var_dump(isset($a['b']));
+
+try {
+    var_dump(isset($a['b']));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
 $a = '0';
-var_dump(isset($a['b']));
+
+try {
+    var_dump(isset($a['b']));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
 
 $simpleString = "Bogus String Text";
 echo isset($simpleString->wrong)?"bug\n":"ok\n";
@@ -46,10 +61,18 @@ echo $simpleString["0"] === "B"?"ok\n":"bug\n";
 bool(false)
 bool(false)
 bool(false)
+
+Warning: Cannot access offset of type string in isset or empty in %s on line %d
 bool(false)
+
+Warning: Cannot access offset of type string in isset or empty in %s on line %d
 bool(false)
+
+Warning: Cannot access offset of type string in isset or empty in %s on line %d
 bool(false)
 ok
+
+Warning: Cannot access offset of type string in isset or empty in %s on line %d
 ok
 ok
 ok
diff --git a/Zend/tests/offsets_isset_empty/bug60362.phpt b/Zend/tests/offsets_isset_empty/bug60362.phpt
new file mode 100644
index 000000000000..88473bef3149
--- /dev/null
+++ b/Zend/tests/offsets_isset_empty/bug60362.phpt
@@ -0,0 +1,99 @@
+--TEST--
+Bug #60362: non-existent sub-sub keys should not have values
+--FILE--
+<?php
+$arr = array('exists' => 'foz');
+
+try {
+    if (isset($arr['exists']['non_existent'])) {
+        echo "sub-key 'non_existent' is set: ";
+        var_dump($arr['exists']['non_existent']);
+    } else {
+        echo "sub-key 'non_existent' is not set.\n";
+    }
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+
+if (isset($arr['exists'][1])) {
+    echo "sub-key 1 is set: ";
+    var_dump($arr['exists'][1]);
+} else {
+    echo "sub-key 1 is not set.\n";
+}
+
+echo "-------------------\n";
+try {
+    if (isset($arr['exists']['non_existent']['sub_sub'])) {
+        echo "sub-key 'sub_sub' is set: ";
+        var_dump($arr['exists']['non_existent']['sub_sub']);
+    } else {
+        echo "sub-sub-key 'sub_sub' is not set.\n";
+    }
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+if (isset($arr['exists'][1][0])) {
+    echo "sub-sub-key 0 is set: ";
+    var_dump($arr['exists'][1][0]);
+} else {
+    echo "sub-sub-key 0 is not set.\n";
+}
+
+echo "-------------------\n";
+try {
+    if (empty($arr['exists']['non_existent'])) {
+        echo "sub-key 'non_existent' is empty.\n";
+    } else {
+        echo "sub-key 'non_existent' is not empty: ";
+        var_dump($arr['exists']['non_existent']);
+    }
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+if (empty($arr['exists'][1])) {
+    echo "sub-key 1 is empty.\n";
+} else {
+    echo "sub-key 1 is not empty: ";
+    var_dump($arr['exists'][1]);
+}
+
+echo "-------------------\n";
+try {
+    if (empty($arr['exists']['non_existent']['sub_sub'])) {
+        echo "sub-sub-key 'sub_sub' is empty.\n";
+    } else {
+        echo "sub-sub-key 'sub_sub' is not empty: ";
+        var_dump($arr['exists']['non_existent']['sub_sub']);
+    }
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+if (empty($arr['exists'][1][0])) {
+    echo "sub-sub-key 0 is empty.\n";
+} else {
+    echo "sub-sub-key 0 is not empty: ";
+    var_dump($arr['exists'][1][0]);
+}
+echo "DONE";
+?>
+--EXPECTF--
+Warning: Cannot access offset of type string in isset or empty in %s on line %d
+sub-key 'non_existent' is not set.
+sub-key 1 is set: string(1) "o"
+-------------------
+
+Warning: Cannot access offset of type string in isset or empty in %s on line %d
+sub-sub-key 'sub_sub' is not set.
+sub-sub-key 0 is set: string(1) "o"
+-------------------
+
+Warning: Cannot access offset of type string in isset or empty in %s on line %d
+sub-key 'non_existent' is empty.
+sub-key 1 is not empty: string(1) "o"
+-------------------
+
+Warning: Cannot access offset of type string in isset or empty in %s on line %d
+sub-sub-key 'sub_sub' is empty.
+sub-sub-key 0 is not empty: string(1) "o"
+DONE
diff --git a/Zend/tests/offsets_isset_empty/bug62680.phpt b/Zend/tests/offsets_isset_empty/bug62680.phpt
new file mode 100644
index 000000000000..05bdc24650b0
--- /dev/null
+++ b/Zend/tests/offsets_isset_empty/bug62680.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Bug #62680 (Function isset() throws fatal error on set array if non-existent key depth >= 3)
+--FILE--
+<?php
+$array = array("");
+try {
+    var_dump(isset($array[0]["a"]["b"]));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+try {
+    var_dump(isset($array[0]["a"]["b"]["c"]));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+?>
+--EXPECTF--
+Warning: Cannot access offset of type string in isset or empty in %s on line %d
+bool(false)
+
+Warning: Cannot access offset of type string in isset or empty in %s on line %d
+bool(false)
diff --git a/Zend/tests/offsets_isset_empty/bug69889.phpt b/Zend/tests/offsets_isset_empty/bug69889.phpt
new file mode 100644
index 000000000000..fcc5132dad94
--- /dev/null
+++ b/Zend/tests/offsets_isset_empty/bug69889.phpt
@@ -0,0 +1,34 @@
+--TEST--
+Bug #69889: Null coalesce operator doesn't work for string offsets
+--FILE--
+<?php
+
+$foo = "test";
+var_dump($foo[0] ?? "default");
+
+var_dump($foo[5] ?? "default");
+var_dump(isset($foo[5]) ? $foo[5] : "default");
+
+try {
+    var_dump($foo["str"] ?? "default");
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+
+try {
+    var_dump(isset($foo["str"]) ? $foo["str"] : "default");
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+
+?>
+--EXPECTF--
+string(1) "t"
+string(7) "default"
+string(7) "default"
+
+Warning: Cannot access offset of type string in isset or empty in %s on line %d
+string(7) "default"
+
+Warning: Cannot access offset of type string in isset or empty in %s on line %d
+string(7) "default"
diff --git a/Zend/tests/offsets_isset_empty/bug81160.phpt b/Zend/tests/offsets_isset_empty/bug81160.phpt
new file mode 100644
index 000000000000..80271a547450
--- /dev/null
+++ b/Zend/tests/offsets_isset_empty/bug81160.phpt
@@ -0,0 +1,110 @@
+--TEST--
+Bug #81160: isset/empty doesn't throw a TypeError on invalid string offset which is inconsistent compared to arrays
+--FILE--
+<?php
+
+$s = 'Hello';
+$o = new stdClass();
+$a = [];
+$r = STDERR;
+
+echo "Objects as offsets:\n";
+try {
+    var_dump(isset($s[$o]));
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+try {
+    var_dump(empty($s[$o]));
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+try {
+    var_dump($s[$o] ?? 'default');
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+echo 'Array ($variable) as offsets:', "\n";
+try {
+    var_dump(isset($s[$a]));
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+try {
+    var_dump(empty($s[$a]));
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+try {
+    var_dump($s[$a] ?? 'default');
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+echo "Array (constant empty array) as offsets:\n";
+try {
+    var_dump(isset($s[[]]));
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+try {
+    var_dump(empty($s[[]]));
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+try {
+    var_dump($s[[]] ?? 'default');
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+echo "Resource as offsets:\n";
+try {
+    var_dump(isset($s[$r]));
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+try {
+    var_dump(empty($s[$r]));
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+try {
+    var_dump($s[$r] ?? 'default');
+} catch (\Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+Objects as offsets:
+
+Warning: Cannot access offset of type stdClass in isset or empty in %s on line %d
+bool(false)
+
+Warning: Cannot access offset of type stdClass in isset or empty in %s on line %d
+bool(true)
+Cannot access offset of type stdClass in isset or empty
+Array ($variable) as offsets:
+
+Warning: Cannot access offset of type array in isset or empty in %s on line %d
+bool(false)
+
+Warning: Cannot access offset of type array in isset or empty in %s on line %d
+bool(true)
+Cannot access offset of type array in isset or empty
+Array (constant empty array) as offsets:
+
+Warning: Cannot access offset of type array in isset or empty in %s on line %d
+bool(false)
+
+Warning: Cannot access offset of type array in isset or empty in %s on line %d
+bool(true)
+Cannot access offset of type array in isset or empty
+Resource as offsets:
+
+Warning: Cannot access offset of type resource in isset or empty in %s on line %d
+bool(false)
+
+Warning: Cannot access offset of type resource in isset or empty in %s on line %d
+bool(true)
+Cannot access offset of type resource in isset or empty
+
diff --git a/Zend/tests/offsets_isset_empty/empty_str_offset.phpt b/Zend/tests/offsets_isset_empty/empty_str_offset.phpt
new file mode 100644
index 000000000000..99f315c210bc
--- /dev/null
+++ b/Zend/tests/offsets_isset_empty/empty_str_offset.phpt
@@ -0,0 +1,207 @@
+--TEST--
+Testing empty() with string offsets
+--FILE--
+<?php
+
+print "- empty ---\n";
+
+$str = "test0123";
+
+var_dump(empty($str[-1]));
+var_dump(empty($str[-10]));
+var_dump(empty($str[-4])); // 0
+var_dump(empty($str[0]));
+var_dump(empty($str[1]));
+var_dump(empty($str[4])); // 0
+var_dump(empty($str[5])); // 1
+var_dump(empty($str[8]));
+var_dump(empty($str[10000]));
+// non-numeric offsets
+print "- string literal ---\n";
+var_dump(empty($str['-1'])); // 3
+var_dump(empty($str['-10']));
+var_dump(empty($str['0']));
+var_dump(empty($str['1']));
+var_dump(empty($str['4'])); // 0
+try {
+    var_dump(empty($str['1.5']));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+try {
+    var_dump(empty($str['good']));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+try {
+    var_dump(empty($str['3 and a half']));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+print "- string variable ---\n";
+var_dump(empty($str[$key = '-1'])); // 3
+var_dump(empty($str[$key = '-10']));
+var_dump(empty($str[$key = '0']));
+var_dump(empty($str[$key = '1']));
+var_dump(empty($str[$key = '4'])); // 0
+try {
+    var_dump(empty($str[$key = '1.5']));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+try {
+    var_dump(empty($str[$key = 'good']));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+try {
+    var_dump(empty($str[$key = '3 and a half']));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+print "- bool ---\n";
+var_dump(empty($str[true]));
+var_dump(empty($str[false]));
+echo "Sub-keys:\n";
+var_dump(empty($str[false][true]));
+print "- null ---\n";
+var_dump(empty($str[null]));
+print "- double ---\n";
+var_dump(empty($str[-1.1]));
+var_dump(empty($str[-10.5]));
+var_dump(empty($str[-4.1]));
+var_dump(empty($str[-0.8]));
+var_dump(empty($str[-0.1]));
+var_dump(empty($str[0.2]));
+var_dump(empty($str[0.9]));
+var_dump(empty($str[M_PI]));
+var_dump(empty($str[100.5001]));
+print "- array ---\n";
+try {
+    var_dump(empty($str[array()]));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+try {
+    var_dump(empty($str[array(1,2,3)]));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+print "- object ---\n";
+try {
+    var_dump(empty($str[new stdClass()]));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+print "- resource ---\n";
+$f = fopen(__FILE__, 'r');
+try {
+    var_dump(empty($str[$f]));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+print "done\n";
+
+?>
+--EXPECTF--
+- empty ---
+bool(false)
+bool(true)
+bool(true)
+bool(false)
+bool(false)
+bool(true)
+bool(false)
+bool(true)
+bool(true)
+- string literal ---
+bool(false)
+bool(true)
+bool(false)
+bool(false)
+bool(true)
+
+Warning: Cannot access offset of type string in isset or empty in %s on line %d
+bool(true)
+
+Warning: Cannot access offset of type string in isset or empty in %s on line %d
+bool(true)
+
+Warning: Illegal string offset "3 and a half" in %s on line %d
+bool(true)
+- string variable ---
+bool(false)
+bool(true)
+bool(false)
+bool(false)
+bool(true)
+
+Warning: Cannot access offset of type string in isset or empty in %s on line %d
+bool(true)
+
+Warning: Cannot access offset of type string in isset or empty in %s on line %d
+bool(true)
+
+Warning: Illegal string offset "3 and a half" in %s on line %d
+bool(true)
+- bool ---
+
+Warning: String offset cast occurred in %s on line %d
+bool(false)
+
+Warning: String offset cast occurred in %s on line %d
+bool(false)
+Sub-keys:
+
+Warning: String offset cast occurred in %s on line %d
+
+Warning: String offset cast occurred in %s on line %d
+bool(true)
+- null ---
+
+Warning: String offset cast occurred in %s on line %d
+bool(false)
+- double ---
+
+Warning: String offset cast occurred in %s on line %d
+bool(false)
+
+Warning: String offset cast occurred in %s on line %d
+bool(true)
+
+Warning: String offset cast occurred in %s on line %d
+bool(true)
+
+Warning: String offset cast occurred in %s on line %d
+bool(false)
+
+Warning: String offset cast occurred in %s on line %d
+bool(false)
+
+Warning: String offset cast occurred in %s on line %d
+bool(false)
+
+Warning: String offset cast occurred in %s on line %d
+bool(false)
+
+Warning: String offset cast occurred in %s on line %d
+bool(false)
+
+Warning: String offset cast occurred in %s on line %d
+bool(true)
+- array ---
+
+Warning: Cannot access offset of type array in isset or empty in %s on line %d
+bool(true)
+
+Warning: Cannot access offset of type array in isset or empty in %s on line %d
+bool(true)
+- object ---
+
+Warning: Cannot access offset of type stdClass in isset or empty in %s on line %d
+bool(true)
+- resource ---
+
+Warning: Cannot access offset of type resource in isset or empty in %s on line %d
+bool(true)
+done
diff --git a/Zend/tests/offsets_isset_empty/isset_str_offset.phpt b/Zend/tests/offsets_isset_empty/isset_str_offset.phpt
new file mode 100644
index 000000000000..6516cca9c56f
--- /dev/null
+++ b/Zend/tests/offsets_isset_empty/isset_str_offset.phpt
@@ -0,0 +1,201 @@
+--TEST--
+Testing isset with string offsets
+--FILE--
+<?php
+
+print "- isset ---\n";
+
+$str = "test0123";
+
+var_dump(isset($str[-1]));
+var_dump(isset($str[-10]));
+var_dump(isset($str[0]));
+var_dump(isset($str[1]));
+var_dump(isset($str[4])); // 0
+var_dump(isset($str[5])); // 1
+var_dump(isset($str[8]));
+var_dump(isset($str[10000]));
+// non-numeric offsets
+print "- string literal ---\n";
+var_dump(isset($str['-1'])); // 3
+var_dump(isset($str['-10']));
+var_dump(isset($str['0']));
+var_dump(isset($str['1']));
+var_dump(isset($str['4'])); // 0
+try {
+    var_dump(isset($str['1.5']));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+try {
+    var_dump(isset($str['good']));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+try {
+    var_dump(isset($str['3 and a half']));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+print "- string variable ---\n";
+var_dump(isset($str[$key = '-1'])); // 3
+var_dump(isset($str[$key = '-10']));
+var_dump(isset($str[$key = '0']));
+var_dump(isset($str[$key = '1']));
+var_dump(isset($str[$key = '4'])); // 0
+try {
+    var_dump(isset($str[$key = '1.5']));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+try {
+    var_dump(isset($str[$key = 'good']));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+try {
+    var_dump(isset($str[$key = '3 and a half']));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+print "- bool ---\n";
+var_dump(isset($str[true]));
+var_dump(isset($str[false]));
+echo "Sub-keys:\n";
+var_dump(isset($str[false][true]));
+print "- null ---\n";
+var_dump(isset($str[null]));
+print "- double ---\n";
+var_dump(isset($str[-1.1]));
+var_dump(isset($str[-10.5]));
+var_dump(isset($str[-0.8]));
+var_dump(isset($str[-0.1]));
+var_dump(isset($str[0.2]));
+var_dump(isset($str[0.9]));
+var_dump(isset($str[M_PI]));
+var_dump(isset($str[100.5001]));
+print "- array ---\n";
+try {
+    var_dump(isset($str[array()]));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+try {
+    var_dump(isset($str[array(1,2,3)]));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+print "- object ---\n";
+try {
+    var_dump(isset($str[new stdClass()]));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+print "- resource ---\n";
+$f = fopen(__FILE__, 'r');
+try {
+    var_dump(isset($str[$f]));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
+print "done\n";
+
+?>
+--EXPECTF--
+- isset ---
+bool(true)
+bool(false)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(false)
+bool(false)
+- string literal ---
+bool(true)
+bool(false)
+bool(true)
+bool(true)
+bool(true)
+
+Warning: Cannot access offset of type string in isset or empty in %s on line %d
+bool(false)
+
+Warning: Cannot access offset of type string in isset or empty in %s on line %d
+bool(false)
+
+Warning: Illegal string offset "3 and a half" in %s on line %d
+bool(false)
+- string variable ---
+bool(true)
+bool(false)
+bool(true)
+bool(true)
+bool(true)
+
+Warning: Cannot access offset of type string in isset or empty in %s on line %d
+bool(false)
+
+Warning: Cannot access offset of type string in isset or empty in %s on line %d
+bool(false)
+
+Warning: Illegal string offset "3 and a half" in %s on line %d
+bool(false)
+- bool ---
+
+Warning: String offset cast occurred in %s on line %d
+bool(true)
+
+Warning: String offset cast occurred in %s on line %d
+bool(true)
+Sub-keys:
+
+Warning: String offset cast occurred in %s on line %d
+
+Warning: String offset cast occurred in %s on line %d
+bool(false)
+- null ---
+
+Warning: String offset cast occurred in %s on line %d
+bool(true)
+- double ---
+
+Warning: String offset cast occurred in %s on line %d
+bool(true)
+
+Warning: String offset cast occurred in %s on line %d
+bool(false)
+
+Warning: String offset cast occurred in %s on line %d
+bool(true)
+
+Warning: String offset cast occurred in %s on line %d
+bool(true)
+
+Warning: String offset cast occurred in %s on line %d
+bool(true)
+
+Warning: String offset cast occurred in %s on line %d
+bool(true)
+
+Warning: String offset cast occurred in %s on line %d
+bool(true)
+
+Warning: String offset cast occurred in %s on line %d
+bool(false)
+- array ---
+
+Warning: Cannot access offset of type array in isset or empty in %s on line %d
+bool(false)
+
+Warning: Cannot access offset of type array in isset or empty in %s on line %d
+bool(false)
+- object ---
+
+Warning: Cannot access offset of type stdClass in isset or empty in %s on line %d
+bool(false)
+- resource ---
+
+Warning: Cannot access offset of type resource in isset or empty in %s on line %d
+bool(false)
+done
diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c
index 1819d0b671dc..26a322597226 100644
--- a/Zend/zend_execute.c
+++ b/Zend/zend_execute.c
@@ -1677,6 +1677,60 @@ static zend_never_inline zend_long zend_check_string_offset(zval *dim, int type
 	return zval_get_long_func(dim, /* is_strict */ false);
 }
 
+/* This is a copy of zend_check_string_offset() use for BP_VAR_IS operations.
+ * Compared to the behaviour of array offsets, isset()/empty() did not throw
+ * TypeErrors for invalid offsets, or warn on type coercions.
+ * The coalesce operator did throw on invalid offset types but not for type coercions. */
+static zend_never_inline zend_long zend_check_string_offset_is_ops(zval *dim, bool *is_type_valid, bool is_coalesce EXECUTE_DATA_DC)
+{
+	zend_long offset;
+	*is_type_valid = true;
+
+try_again:
+	switch(Z_TYPE_P(dim)) {
+		case IS_LONG:
+			return Z_LVAL_P(dim);
+		case IS_STRING:
+		{
+			bool trailing_data = false;
+			/* For BC reasons we allow errors so that we can warn on leading numeric string */
+			if (IS_LONG == is_numeric_string_ex(Z_STRVAL_P(dim), Z_STRLEN_P(dim), &offset, NULL,
+					/* allow errors */ true, NULL, &trailing_data)) {
+				if (UNEXPECTED(trailing_data)) {
+					*is_type_valid = false;
+					zend_error(E_WARNING, "Illegal string offset \"%s\"", Z_STRVAL_P(dim));
+				}
+				return offset;
+			}
+			*is_type_valid = false;
+			zend_error(E_WARNING, "Cannot access offset of type %s in isset or empty", zend_zval_type_name(dim));
+			return 0;
+		}
+		case IS_UNDEF:
+			ZVAL_UNDEFINED_OP2();
+			ZEND_FALLTHROUGH;
+		case IS_DOUBLE:
+		case IS_NULL:
+		case IS_FALSE:
+		case IS_TRUE:
+			zend_error(E_WARNING, "String offset cast occurred");
+			break;
+		case IS_REFERENCE:
+			dim = Z_REFVAL_P(dim);
+			goto try_again;
+		default:
+			*is_type_valid = false;
+			if (is_coalesce) {
+				zend_illegal_string_offset(dim, BP_VAR_IS);
+			} else {
+				zend_error(E_WARNING, "Cannot access offset of type %s in isset or empty", zend_zval_type_name(dim));
+			}
+			return 0;
+	}
+
+	return zval_get_long_func(dim, /* is_strict */ false);
+}
+
 ZEND_API ZEND_COLD void zend_wrong_string_offset_error(void)
 {
 	const char *msg = NULL;
@@ -2747,73 +2801,35 @@ static zend_always_inline void zend_fetch_dimension_address_read(zval *result, z
 		zend_string *str = Z_STR_P(container);
 		zend_long offset;
 
-try_string_offset:
 		if (UNEXPECTED(Z_TYPE_P(dim) != IS_LONG)) {
-			switch (Z_TYPE_P(dim)) {
-				case IS_STRING:
-				{
-					bool trailing_data = false;
-					/* For BC reasons we allow errors so that we can warn on leading numeric string */
-					if (IS_LONG == is_numeric_string_ex(Z_STRVAL_P(dim), Z_STRLEN_P(dim), &offset,
-							NULL, /* allow errors */ true, NULL, &trailing_data)) {
-						if (UNEXPECTED(trailing_data)) {
-							zend_error(E_WARNING, "Illegal string offset \"%s\"", Z_STRVAL_P(dim));
-						}
-						goto out;
-					}
-					if (type == BP_VAR_IS) {
-						ZVAL_NULL(result);
-						return;
-					}
-					zend_illegal_string_offset(dim, BP_VAR_R);
-					ZVAL_NULL(result);
-					return;
-				}
-				case IS_UNDEF:
-					/* The string may be destroyed while throwing the notice.
-					 * Temporarily increase the refcount to detect this situation. */
-					if (!(GC_FLAGS(str) & IS_STR_INTERNED)) {
-						GC_ADDREF(str);
-					}
-					ZVAL_UNDEFINED_OP2();
-					if (!(GC_FLAGS(str) & IS_STR_INTERNED) && UNEXPECTED(GC_DELREF(str) == 0)) {
-						zend_string_efree(str);
-						ZVAL_NULL(result);
-						return;
-					}
-					ZEND_FALLTHROUGH;
-				case IS_DOUBLE:
-				case IS_NULL:
-				case IS_FALSE:
-				case IS_TRUE:
-					if (type != BP_VAR_IS) {
-						/* The string may be destroyed while throwing the notice.
-						 * Temporarily increase the refcount to detect this situation. */
-						if (!(GC_FLAGS(str) & IS_STR_INTERNED)) {
-							GC_ADDREF(str);
-						}
-						zend_error(E_WARNING, "String offset cast occurred");
-						if (!(GC_FLAGS(str) & IS_STR_INTERNED) && UNEXPECTED(GC_DELREF(str) == 0)) {
-							zend_string_efree(str);
-							ZVAL_NULL(result);
-							return;
-						}
-					}
-					break;
-				case IS_REFERENCE:
-					dim = Z_REFVAL_P(dim);
-					goto try_string_offset;
-				default:
-					zend_illegal_string_offset(dim, BP_VAR_R);
-					ZVAL_NULL(result);
-					return;
+			/* The string may be destroyed while throwing the notice.
+			 * Temporarily increase the refcount to detect this situation. */
+			if (!(GC_FLAGS(str) & IS_STR_INTERNED)) {
+				GC_ADDREF(str);
+			}
+			/* Coalesce operator didn't behave like isset()/empty() in that a
+			 * TypeError was thrown if the offset was of type array/resource/object
+			 * However, null/bool/float type coercion warnings were suppressed. */
+			bool is_type_valid = true;
+			if (type == BP_VAR_IS) {
+				offset = zend_check_string_offset_is_ops(dim, &is_type_valid, /* is_coalesce */ true EXECUTE_DATA_CC);
+			} else {
+				offset = zend_check_string_offset(dim, dim_type EXECUTE_DATA_CC);
 			}
 
-			offset = zval_get_long_func(dim, /* is_strict */ false);
+			if (!(GC_FLAGS(str) & IS_STR_INTERNED) && UNEXPECTED(GC_DELREF(str) == 0)) {
+				zend_string_efree(str);
+				ZVAL_NULL(result);
+				return;
+			}
+			/* Illegal offset assignment */
+			if (!is_type_valid || UNEXPECTED(EG(exception) != NULL)) {
+				ZVAL_NULL(result);
+				return;
+			}
 		} else {
 			offset = Z_LVAL_P(dim);
 		}
-		out:
 
 		if (UNEXPECTED(ZSTR_LEN(str) < ((offset < 0) ? -(size_t)offset : ((size_t)offset + 1)))) {
 			if (type != BP_VAR_IS) {
@@ -2957,15 +2973,17 @@ static zend_never_inline bool ZEND_FASTCALL zend_isset_dim_slow(zval *container,
 			/*if (OP2_TYPE & (IS_CV|IS_VAR)) {*/
 				ZVAL_DEREF(offset);
 			/*}*/
-			if (Z_TYPE_P(offset) < IS_STRING /* simple scalar types */
-					|| (Z_TYPE_P(offset) == IS_STRING /* or numeric string */
-						&& IS_LONG == is_numeric_string(Z_STRVAL_P(offset), Z_STRLEN_P(offset), NULL, NULL, 0))) {
-				lval = zval_get_long_ex(offset, /* is_strict */ true);
-				goto str_offset;
+
+			bool is_type_valid = true;
+			/* For BC we currently emit E_WARNINGs */
+			lval = zend_check_string_offset_is_ops(offset, &is_type_valid, /* is_coalesce */ false EXECUTE_DATA_CC);
+			if (!is_type_valid || UNEXPECTED(EG(exception) != NULL)) {
+				return false;
 			}
-			return 0;
+			goto str_offset;
 		}
 	} else {
+		/* Container is invalid, TODO deprecate this? */
 		return 0;
 	}
 }
@@ -2996,15 +3014,17 @@ static zend_never_inline bool ZEND_FASTCALL zend_isempty_dim_slow(zval *containe
 			/*if (OP2_TYPE & (IS_CV|IS_VAR)) {*/
 				ZVAL_DEREF(offset);
 			/*}*/
-			if (Z_TYPE_P(offset) < IS_STRING /* simple scalar types */
-					|| (Z_TYPE_P(offset) == IS_STRING /* or numeric string */
-						&& IS_LONG == is_numeric_string(Z_STRVAL_P(offset), Z_STRLEN_P(offset), NULL, NULL, 0))) {
-				lval = zval_get_long_ex(offset, /* is_strict */ true);
-				goto str_offset;
+
+			bool is_type_valid = true;
+			/* For BC we currently emit E_WARNINGs */
+			lval = zend_check_string_offset_is_ops(offset, &is_type_valid, /* is_coalesce */ false EXECUTE_DATA_CC);
+			if (!is_type_valid || UNEXPECTED(EG(exception) != NULL)) {
+				return true;
 			}
-			return 1;
+			goto str_offset;
 		}
 	} else {
+		/* Container is invalid, TODO deprecate this? */
 		return 1;
 	}
 }
diff --git a/ext/opcache/jit/zend_jit_helpers.c b/ext/opcache/jit/zend_jit_helpers.c
index 7c80de916aac..84371770979c 100644
--- a/ext/opcache/jit/zend_jit_helpers.c
+++ b/ext/opcache/jit/zend_jit_helpers.c
@@ -1058,6 +1058,60 @@ static zend_never_inline zend_long zend_check_string_offset(zval *dim, int type)
 	return zval_get_long_func(dim, /* is_strict */ false);
 }
 
+/* This is a copy of zend_check_string_offset() use for BP_VAR_IS operations.
+ * Compared to the behaviour of array offsets, isset()/empty() did not throw
+ * TypeErrors for invalid offsets, or warn on type coercions.
+ * The coalesce operator did throw on invalid offset types but not for type coercions. */
+static zend_never_inline zend_long zend_check_string_offset_is_ops(zval *dim, bool *is_type_valid, bool is_coalesce)
+{
+	zend_long offset;
+	*is_type_valid = true;
+
+try_again:
+	switch(Z_TYPE_P(dim)) {
+		case IS_LONG:
+			return Z_LVAL_P(dim);
+		case IS_STRING:
+		{
+			bool trailing_data = false;
+			/* For BC reasons we allow errors so that we can warn on leading numeric string */
+			if (IS_LONG == is_numeric_string_ex(Z_STRVAL_P(dim), Z_STRLEN_P(dim), &offset, NULL,
+					/* allow errors */ true, NULL, &trailing_data)) {
+				if (UNEXPECTED(trailing_data)) {
+					*is_type_valid = false;
+					zend_error(E_WARNING, "Illegal string offset \"%s\"", Z_STRVAL_P(dim));
+				}
+				return offset;
+			}
+			*is_type_valid = false;
+			zend_error(E_WARNING, "Cannot access offset of type %s in isset or empty", zend_zval_type_name(dim));
+			return 0;
+		}
+		case IS_UNDEF:
+			zend_jit_undefined_op_helper(EG(current_execute_data)->opline->op2.var);
+			ZEND_FALLTHROUGH;
+		case IS_DOUBLE:
+		case IS_NULL:
+		case IS_FALSE:
+		case IS_TRUE:
+			zend_error(E_WARNING, "String offset cast occurred");
+			break;
+		case IS_REFERENCE:
+			dim = Z_REFVAL_P(dim);
+			goto try_again;
+		default:
+			*is_type_valid = false;
+			if (is_coalesce) {
+				zend_illegal_container_offset(ZSTR_KNOWN(ZEND_STR_STRING), dim, BP_VAR_IS);
+			} else {
+				zend_error(E_WARNING, "Cannot access offset of type %s in isset or empty", zend_zval_type_name(dim));
+			}
+			return 0;
+	}
+
+	return zval_get_long_func(dim, /* is_strict */ false);
+}
+
 static zend_always_inline zend_string* zend_jit_fetch_dim_str_offset(zend_string *str, zend_long offset)
 {
 	if (UNEXPECTED((zend_ulong)offset >= (zend_ulong)ZSTR_LEN(str))) {
@@ -1105,32 +1159,17 @@ static void ZEND_FASTCALL zend_jit_fetch_dim_str_is_helper(zend_string *str, zva
 {
 	zend_long offset;
 
-try_string_offset:
 	if (UNEXPECTED(Z_TYPE_P(dim) != IS_LONG)) {
-		switch (Z_TYPE_P(dim)) {
-			/* case IS_LONG: */
-			case IS_STRING:
-				if (IS_LONG == is_numeric_string(Z_STRVAL_P(dim), Z_STRLEN_P(dim), NULL, NULL, false)) {
-					break;
-				}
-				ZVAL_NULL(result);
-				return;
-			case IS_UNDEF:
-				zend_jit_undefined_op_helper(EG(current_execute_data)->opline->op2.var);
-			case IS_DOUBLE:
-			case IS_NULL:
-			case IS_FALSE:
-			case IS_TRUE:
-				break;
-			case IS_REFERENCE:
-				dim = Z_REFVAL_P(dim);
-				goto try_string_offset;
-			default:
-				zend_illegal_container_offset(ZSTR_KNOWN(ZEND_STR_STRING), dim, BP_VAR_IS);
-				break;
+		bool is_type_valid = true;
+		/* Coalesce operator didn't behave like isset()/empty() in that a
+		 * TypeError was thrown if the offset was of type array/resource/object
+		 * However, null/bool/float type coercion warnings were suppressed. */
+		offset = zend_check_string_offset_is_ops(dim, &is_type_valid, /* is_coalesce */ true);
+		/* Illegal offset */
+		if (!is_type_valid || UNEXPECTED(EG(exception) != NULL)) {
+			ZVAL_NULL(result);
+			return;
 		}
-
-		offset = zval_get_long_func(dim, /* is_strict */ false);
 	} else {
 		offset = Z_LVAL_P(dim);
 	}
@@ -1736,12 +1775,13 @@ static int ZEND_FASTCALL zend_jit_isset_dim_helper(zval *container, zval *offset
 			}
 		} else {
 			ZVAL_DEREF(offset);
-			if (Z_TYPE_P(offset) < IS_STRING /* simple scalar types */
-					|| (Z_TYPE_P(offset) == IS_STRING /* or numeric string */
-						&& IS_LONG == is_numeric_string(Z_STRVAL_P(offset), Z_STRLEN_P(offset), NULL, NULL, false))) {
-				lval = zval_get_long_ex(offset, /* is_strict */ true);
-				goto isset_str_offset;
+			bool is_type_valid = true;
+			/* For BC we currently emit E_WARNINGs */
+			lval = zend_check_string_offset_is_ops(offset, &is_type_valid, /* is_coalesce */ false);
+			if (!is_type_valid || UNEXPECTED(EG(exception) != NULL)) {
+				return 0;
 			}
+			goto isset_str_offset;
 		}
 	}
 	return 0;
diff --git a/tests/strings/offsets_chaining_5.phpt b/tests/strings/offsets_chaining_5.phpt
index 49f062463f1e..02da34820bc8 100644
--- a/tests/strings/offsets_chaining_5.phpt
+++ b/tests/strings/offsets_chaining_5.phpt
@@ -5,18 +5,30 @@ testing the behavior of string offset chaining
 $array = array('expected_array' => "foobar");
 var_dump(isset($array['expected_array']));
 var_dump($array['expected_array']);
-var_dump(isset($array['expected_array']['foo']));
+try {
+    var_dump(isset($array['expected_array']['foo']));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
 var_dump($array['expected_array']['0foo']);
-var_dump(isset($array['expected_array']['foo']['bar']));
+try {
+    var_dump(isset($array['expected_array']['foo']['bar']));
+} catch (\TypeError $e) {
+    echo $e->getMessage(), \PHP_EOL;
+}
 var_dump($array['expected_array']['0foo']['0bar']);
 ?>
 --EXPECTF--
 bool(true)
 string(6) "foobar"
+
+Warning: Cannot access offset of type string in isset or empty in %s on line %d
 bool(false)
 
 Warning: Illegal string offset "0foo" in %s on line %d
 string(1) "f"
+
+Warning: Cannot access offset of type string in isset or empty in %s on line %d
 bool(false)
 
 Warning: Illegal string offset "0foo" in %s on line %d
diff --git a/tests/strings/offsets_general.phpt b/tests/strings/offsets_general.phpt
index 16960eac9522..1cc37f0c72b9 100644
--- a/tests/strings/offsets_general.phpt
+++ b/tests/strings/offsets_general.phpt
@@ -14,14 +14,20 @@ try {
 } catch (\TypeError $e) {
     echo $e->getMessage() . \PHP_EOL;
 }
-var_dump(isset($string["foo"]["bar"]));
+try {
+    var_dump(isset($string["foo"]["bar"]));
+} catch (\TypeError $e) {
+    echo $e->getMessage() . \PHP_EOL;
+}
 
 ?>
---EXPECT--
+--EXPECTF--
 string(1) "B"
 string(1) "f"
 string(1) "o"
 bool(true)
 bool(true)
 Cannot access offset of type string on string
+
+Warning: Cannot access offset of type string in isset or empty in %s on line %d
 bool(false)