Leo's dev blog

How (and why) to parse a function from a string in JavaScript/TypeScript

Published on
Published on
/4 mins read/---

So I faced a problem at work today: when trying to send a JSON response containing objects with functions as properties from the server to the client, the functions got lost, they became undefined in the client code. This is because JSON does not support functions, so they get stripped out during serialization (See the JSON.stringify() description on MDN web docs for more details).

json stringify description

So I found myself a solution is to serialize the functions as strings with Function.prototype.toString(), and then parse them back into callable functions on the client side.

In the server side, you can simply process your object and convert the functions to strings with the toString() method. And on the client side, you need to parse those strings back into functions with a pretty handy utility.

This snippet shows you how to safely parse different function formats from a string and get a callable function back:

parse-function.ts
function parseFunction(funcStr: string): (...args: any[]) => any {
  if (!funcStr || typeof funcStr !== "string") {
    throw new Error("Invalid function string: must be a non-empty string");
  }
 
  // Remove comments and normalize whitespace
  let cleanStr = funcStr
    .replace(/\/\*[\s\S]*?\*\//g, "") // Remove block comments
    .replace(/\/\/.*$/gm, "") // Remove line comments
    .trim();
 
  if (!cleanStr) {
    throw new Error("Invalid function string: empty after cleaning");
  }
 
  let match: RegExpMatchArray | null;
 
  try {
    // Regular function: function name(params) { body } or function(params) { body }
    match = cleanStr.match(
      /^(?:async\s+)?function\s*[^()]*\(([^)]*)\)\s*{([\s\S]*)}$/,
    );
    if (match) {
      let params = match[1]
        .split(",")
        .map((p) => p.trim())
        .filter(Boolean);
      let body = match[2].trim();
      return new Function(...params, body) as (...args: any[]) => any;
    }
 
    // Arrow function with braces: (params) => { body }
    match = cleanStr.match(/^(?:async\s+)?\(([^)]*)\)\s*=>\s*{([\s\S]*)}$/);
    if (match) {
      let params = match[1]
        .split(",")
        .map((p) => p.trim())
        .filter(Boolean);
      let body = match[2].trim();
      return new Function(...params, body) as (...args: any[]) => any;
    }
 
    // Arrow function with single parameter and braces: param => { body }
    match = cleanStr.match(/^(?:async\s+)?([^=\s(]+)\s*=>\s*{([\s\S]*)}$/);
    if (match) {
      let param = match[1].trim();
      let body = match[2].trim();
      return new Function(param, body) as (...args: any[]) => any;
    }
 
    // Simple arrow function with parentheses: (params) => expression
    match = cleanStr.match(/^(?:async\s+)?\(([^)]*)\)\s*=>\s*(.+)$/);
    if (match) {
      let params = match[1]
        .split(",")
        .map((p) => p.trim())
        .filter(Boolean);
      let expression = match[2].trim();
      return new Function(...params, `return (${expression})`) as (
        ...args: any[]
      ) => any;
    }
 
    // Single parameter arrow function: param => expression
    match = cleanStr.match(/^(?:async\s+)?([^=\s(]+)\s*=>\s*(.+)$/);
    if (match) {
      let param = match[1].trim();
      let expression = match[2].trim();
      return new Function(param, `return (${expression})`) as (
        ...args: any[]
      ) => any;
    }
 
    // If no patterns match, throw an error
    throw new Error(`Unsupported function format: ${cleanStr}`);
  } catch (error) {
    if (error instanceof SyntaxError) {
      throw new Error(`Invalid function syntax: ${error.message}`);
    }
    throw new Error(`Failed to create function: ${error.message}`);
  }
}

What is does:

  • Handles regular, arrow, and single-param functions.
  • Cleans up comments and whitespace.
  • Throws helpful errors if the string is invalid.

Better handle the parsing process within a try-catch block to catch any potential errors.

Example:

try {
  let fn1 = parseFunction('(a, b) => a + b')
  let fn2 = parseFunction('function add(a, b) { return a + b; }')
  let fn3 = parseFunction('x => x * 2')
  let fn4 = parseFunction('async (x, y) => { return x + y; }')
  let fn5 = parseFunction('async function multiply(a, b) { return a * b; }')
  console.log(fn1(2, 3)) // 5
  console.log(fn2(2, 3)) // 5
  console.log(fn3(4)) // 8
  console.log(fn4(2, 3)) // 5
  console.log(fn5(2, 3)) // 6
} catch (error) {
  console.error('Failed to parse function:', error)
}

WARNING

Executing a function from a string can be risky! Only use this if you trust the source of the string, as it can execute arbitrary code.

More use cases (Copilot suggested )

  • Serialization: Store and reload functions as strings (for configs, migrations, or state machines).
  • Dynamic scripting: Let users write custom formulas or logic in a web app (think spreadsheet formulas, calculators, or workflow builders).
  • Code editors and sandboxes: Run code snippets entered by users for live previews or interactive docs.
  • Plugins and automation: Allow plugins or extensions to define their own logic as strings.
  • Server-to-client logic: Send a function definition from the server to the client for dynamic execution (for example, custom validation or transformation logic that changes over time).

NOTE

If your site has configs Content Security Policy (CSP), make sure the script-src includes the unsafe-eval directive, otherwise this won't work.

Happy parsing!