diff --git a/app/javascript/components/bootcamp/FrontendExercisePage/LHS/injectLoopguards.ts b/app/javascript/components/bootcamp/FrontendExercisePage/LHS/injectLoopguards.ts
new file mode 100644
index 0000000000..437e21f7d9
--- /dev/null
+++ b/app/javascript/components/bootcamp/FrontendExercisePage/LHS/injectLoopguards.ts
@@ -0,0 +1,127 @@
+import * as acorn from 'acorn'
+import * as walk from 'acorn-walk'
+import * as astring from 'astring'
+
+export function injectLoopGuards(code: string): string {
+  const ast = acorn.parse(code, {
+    ecmaVersion: 2020,
+    sourceType: 'module',
+  }) as acorn.Node
+
+  let loopId = 0
+  const usedGuards: string[] = []
+
+  walk.ancestor(ast, {
+    WhileStatement(node, ancestors: any[]) {
+      const id = `__loop_guard_${loopId++}`
+      usedGuards.push(id)
+      guardLoop(node, ancestors, id)
+    },
+    ForStatement(node, ancestors) {
+      const id = `__loop_guard_${loopId++}`
+      usedGuards.push(id)
+      guardLoop(node, ancestors, id)
+    },
+    DoWhileStatement(node, ancestors) {
+      const id = `__loop_guard_${loopId++}`
+      usedGuards.push(id)
+      guardLoop(node, ancestors, id)
+    },
+  })
+
+  const guardVars = usedGuards.map((id) => `let ${id} = 0;`).join('\n')
+
+  const finalCode = `
+    const __MAX_ITERATIONS = 10000;
+    ${guardVars}
+    ${astring.generate(ast)}
+  `
+
+  return finalCode
+}
+
+function guardLoop(node: any, ancestors: any[], loopVar: string) {
+  /**
+    AST of
+    let ${loopVar} = 0;
+   */
+  const guard = {
+    type: 'VariableDeclaration',
+    kind: 'let',
+    declarations: [
+      {
+        type: 'VariableDeclarator',
+        id: { type: 'Identifier', name: loopVar },
+        init: { type: 'Literal', value: 0 },
+      },
+    ],
+  }
+
+  /*
+  AST of
+  if (++${loopVar} > __MAX_ITERATIONS) throw new Error("Infinite loop detected") 
+   */
+  const check = {
+    type: 'IfStatement',
+    test: {
+      type: 'BinaryExpression',
+      operator: '>',
+      left: {
+        type: 'UpdateExpression',
+        operator: '++',
+        prefix: true,
+        argument: { type: 'Identifier', name: loopVar },
+      },
+      right: { type: 'Identifier', name: '__MAX_ITERATIONS' },
+    },
+    consequent: {
+      type: 'ThrowStatement',
+      argument: {
+        type: 'NewExpression',
+        callee: { type: 'Identifier', name: 'Error' },
+        arguments: [{ type: 'Literal', value: 'Infinite loop detected' }],
+      },
+    },
+  }
+
+  // we check if the loop actually has a block body
+  // and if it doesn't, we wrap it in a block statement
+  // otherwise after the injection the code would be invalid JS.
+
+  // code below turns this:
+
+  // while (true) doSomething();
+  // into this:
+  // while (true) {
+  //   if (++__loop_guard_0 > __MAX_ITERATIONS) throw new Error(...);
+  //   doSomething();
+  // }
+  if (node.body.type !== 'BlockStatement') {
+    node.body = {
+      type: 'BlockStatement',
+      body: [check, node.body],
+    }
+  } else {
+    node.body.body.unshift(check)
+  }
+
+  // this is needed to make sure loop-guard variable is declared in the parent scope and before the loop
+  // to avoid any reference errors
+  const parentBody = findNearestBody(ancestors)
+  if (parentBody) {
+    const index = parentBody.indexOf(node)
+    if (index !== -1) {
+      parentBody.splice(index, 0, guard)
+    }
+  }
+}
+
+function findNearestBody(ancestors: any[]): any[] | null {
+  for (let i = ancestors.length - 1; i >= 0; i--) {
+    const parent = ancestors[i]
+    if (parent.type === 'BlockStatement') {
+      return parent.body
+    }
+  }
+  return null
+}
diff --git a/app/javascript/components/bootcamp/JikiscriptExercisePage/exercises/wordle/WordleGame.ts b/app/javascript/components/bootcamp/JikiscriptExercisePage/exercises/wordle/WordleGame.ts
index 9815b75306..d4fbb7015c 100644
--- a/app/javascript/components/bootcamp/JikiscriptExercisePage/exercises/wordle/WordleGame.ts
+++ b/app/javascript/components/bootcamp/JikiscriptExercisePage/exercises/wordle/WordleGame.ts
@@ -32,7 +32,6 @@ function fn(this: WordleExercise) {
       word: Jiki.JikiObject,
       states: Jiki.JikiObject
     ) {
-      console.log(row, word, states)
       if (!(row instanceof Jiki.Number)) {
         return executionCtx.logicError('The first input must be a number')
       }
diff --git a/app/javascript/components/bootcamp/JikiscriptExercisePage/test-runner/generateAndRunTestSuite/execJS.ts b/app/javascript/components/bootcamp/JikiscriptExercisePage/test-runner/generateAndRunTestSuite/execJS.ts
index 4dd10d3d5c..d2718c20c4 100644
--- a/app/javascript/components/bootcamp/JikiscriptExercisePage/test-runner/generateAndRunTestSuite/execJS.ts
+++ b/app/javascript/components/bootcamp/JikiscriptExercisePage/test-runner/generateAndRunTestSuite/execJS.ts
@@ -1,5 +1,6 @@
 import { generateCodeRunString } from '../../utils/generateCodeRunString'
 import * as acorn from 'acorn'
+import { injectLoopGuards } from './injectLoopGuards'
 
 const esm = (code: string) =>
   URL.createObjectURL(new Blob([code], { type: 'text/javascript' }))
@@ -43,6 +44,8 @@ export async function execJS(
     }
   }
 
+  // let guardedStudentCode = injectLoopGuards(studentCode)
+
   let code = `
     let currentTime = 0
     const executionCtx = { 
diff --git a/app/javascript/components/bootcamp/JikiscriptExercisePage/test-runner/generateAndRunTestSuite/execTest.ts b/app/javascript/components/bootcamp/JikiscriptExercisePage/test-runner/generateAndRunTestSuite/execTest.ts
index 36b3eecf5d..53a0414e1b 100644
--- a/app/javascript/components/bootcamp/JikiscriptExercisePage/test-runner/generateAndRunTestSuite/execTest.ts
+++ b/app/javascript/components/bootcamp/JikiscriptExercisePage/test-runner/generateAndRunTestSuite/execTest.ts
@@ -69,6 +69,7 @@ export async function execTest(
   let actual: any
   let frames: Frame[] = []
   let evaluated: any = null
+  let hasJSError = false
 
   switch (language) {
     case 'javascript': {
@@ -85,13 +86,13 @@ export async function execTest(
 
       if (result.status === 'error') {
         if (editorView) {
-          console.log(result)
           showError({
             error: result.error,
             ...stateSetters,
             editorView,
           })
         }
+        hasJSError = true
       }
 
       // null falls back to [Your function didn't return anything]
@@ -134,6 +135,16 @@ export async function execTest(
 
   const expects = generateExpects(evaluated, testData, actual, exercise)
 
+  if (hasJSError) {
+    expects.push({
+      actual: 'running',
+      matcher: 'toBe',
+      errorHtml: 'Your code has an error in it.',
+      expected: true,
+      pass: false,
+    })
+  }
+
   return {
     expects,
     slug: testData.slug,
diff --git a/app/javascript/components/bootcamp/JikiscriptExercisePage/test-runner/generateAndRunTestSuite/injectLoopGuards.ts b/app/javascript/components/bootcamp/JikiscriptExercisePage/test-runner/generateAndRunTestSuite/injectLoopGuards.ts
new file mode 100644
index 0000000000..437e21f7d9
--- /dev/null
+++ b/app/javascript/components/bootcamp/JikiscriptExercisePage/test-runner/generateAndRunTestSuite/injectLoopGuards.ts
@@ -0,0 +1,127 @@
+import * as acorn from 'acorn'
+import * as walk from 'acorn-walk'
+import * as astring from 'astring'
+
+export function injectLoopGuards(code: string): string {
+  const ast = acorn.parse(code, {
+    ecmaVersion: 2020,
+    sourceType: 'module',
+  }) as acorn.Node
+
+  let loopId = 0
+  const usedGuards: string[] = []
+
+  walk.ancestor(ast, {
+    WhileStatement(node, ancestors: any[]) {
+      const id = `__loop_guard_${loopId++}`
+      usedGuards.push(id)
+      guardLoop(node, ancestors, id)
+    },
+    ForStatement(node, ancestors) {
+      const id = `__loop_guard_${loopId++}`
+      usedGuards.push(id)
+      guardLoop(node, ancestors, id)
+    },
+    DoWhileStatement(node, ancestors) {
+      const id = `__loop_guard_${loopId++}`
+      usedGuards.push(id)
+      guardLoop(node, ancestors, id)
+    },
+  })
+
+  const guardVars = usedGuards.map((id) => `let ${id} = 0;`).join('\n')
+
+  const finalCode = `
+    const __MAX_ITERATIONS = 10000;
+    ${guardVars}
+    ${astring.generate(ast)}
+  `
+
+  return finalCode
+}
+
+function guardLoop(node: any, ancestors: any[], loopVar: string) {
+  /**
+    AST of
+    let ${loopVar} = 0;
+   */
+  const guard = {
+    type: 'VariableDeclaration',
+    kind: 'let',
+    declarations: [
+      {
+        type: 'VariableDeclarator',
+        id: { type: 'Identifier', name: loopVar },
+        init: { type: 'Literal', value: 0 },
+      },
+    ],
+  }
+
+  /*
+  AST of
+  if (++${loopVar} > __MAX_ITERATIONS) throw new Error("Infinite loop detected") 
+   */
+  const check = {
+    type: 'IfStatement',
+    test: {
+      type: 'BinaryExpression',
+      operator: '>',
+      left: {
+        type: 'UpdateExpression',
+        operator: '++',
+        prefix: true,
+        argument: { type: 'Identifier', name: loopVar },
+      },
+      right: { type: 'Identifier', name: '__MAX_ITERATIONS' },
+    },
+    consequent: {
+      type: 'ThrowStatement',
+      argument: {
+        type: 'NewExpression',
+        callee: { type: 'Identifier', name: 'Error' },
+        arguments: [{ type: 'Literal', value: 'Infinite loop detected' }],
+      },
+    },
+  }
+
+  // we check if the loop actually has a block body
+  // and if it doesn't, we wrap it in a block statement
+  // otherwise after the injection the code would be invalid JS.
+
+  // code below turns this:
+
+  // while (true) doSomething();
+  // into this:
+  // while (true) {
+  //   if (++__loop_guard_0 > __MAX_ITERATIONS) throw new Error(...);
+  //   doSomething();
+  // }
+  if (node.body.type !== 'BlockStatement') {
+    node.body = {
+      type: 'BlockStatement',
+      body: [check, node.body],
+    }
+  } else {
+    node.body.body.unshift(check)
+  }
+
+  // this is needed to make sure loop-guard variable is declared in the parent scope and before the loop
+  // to avoid any reference errors
+  const parentBody = findNearestBody(ancestors)
+  if (parentBody) {
+    const index = parentBody.indexOf(node)
+    if (index !== -1) {
+      parentBody.splice(index, 0, guard)
+    }
+  }
+}
+
+function findNearestBody(ancestors: any[]): any[] | null {
+  for (let i = ancestors.length - 1; i >= 0; i--) {
+    const parent = ancestors[i]
+    if (parent.type === 'BlockStatement') {
+      return parent.body
+    }
+  }
+  return null
+}
diff --git a/package.json b/package.json
index 3e9a822cb4..d2f531ea48 100644
--- a/package.json
+++ b/package.json
@@ -53,6 +53,7 @@
     "ace-builds": "^1.4.12",
     "acorn": "^8.14.1",
     "actioncable": "^5.2.4-3",
+    "astring": "^1.9.0",
     "autoprefixer": "latest",
     "browserslist-to-esbuild": "^1.1.1",
     "canvas-confetti": "^1.9.3",
diff --git a/yarn.lock b/yarn.lock
index ec643ccc40..b60a6c14d1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3033,6 +3033,11 @@ astral-regex@^2.0.0:
   resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
   integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
 
+astring@^1.9.0:
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/astring/-/astring-1.9.0.tgz#cc73e6062a7eb03e7d19c22d8b0b3451fd9bfeef"
+  integrity sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==
+
 async@3.2.3:
   version "3.2.3"
   resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9"