How Lodash _.attempt Handles Errors with Try Catch

This article provides an overview of Lodash's _.attempt utility, detailing how it intercepts script exceptions by abstracting JavaScript's native try-catch mechanics. By wrapping function execution in an internal error-handling boundary, _.attempt converts thrown exceptions into first-class return values, streamlining control flow and eliminating the need for verbose, deeply nested native error-handling blocks.

The Mechanics of _.attempt

In standard JavaScript, unhandled runtime exceptions interrupt execution and bubble up the call stack unless explicitly captured by a try-catch statement. Lodash’s _.attempt utility acts as an execution wrapper designed to intercept these failures at the point of origin.

Under the hood, _.attempt accepts a function along with any arguments that need to be forwarded to it. Internally, it invokes the target function within a native try-catch block:

function attempt(func, ...args) {
  try {
    return func(...args);
  } catch (error) {
    return isError(error) ? error : new Error(error);
  }
}

Instead of allowing an unhandled script error to break the runtime or trigger global error events (such as window.onerror), the utility catches the thrown value directly. If the caught object is not already an instance of an Error, Lodash normalizes it into one before returning it.

Replacing Nested Control Flow

Traditional native try-catch constructs are statements rather than expressions, meaning they cannot directly assign values without declaring mutable variables outside the block:

let data;
try {
  data = JSON.parse(rawJson);
} catch (e) {
  data = null;
}

_.attempt replaces this imperative control flow with a functional, expression-based pattern. It encapsulates the native try-catch logic internally, returning either the successful result of the invoked function or the caught Error object directly to a single assignment target:

const result = _.attempt(JSON.parse, rawJson);

This design isolates parsing and execution failures to the local assignment, preventing unhandled errors from terminating scripts while keeping the codebase linear and readable.

Evaluating Outcomes with _.isError

Because _.attempt captures errors and returns them as plain values, standard execution flow does not divert to an external handler. To process the outcome, developers check the return value using Lodash's complementary _.isError method:

const result = _.attempt(riskyOperation);

if (_.isError(result)) {
  // Handle failure state
  console.error("Execution failed:", result.message);
} else {
  // Proceed with standard execution
  console.log("Success:", result);
}

This pattern mirrors error-handling approaches found in languages with explicit error returns, treating exceptions as predictable data rather than control-flow disruptions.

Synchronous Boundaries and Limitations

The isolation provided by _.attempt is strictly synchronous. The internal try-catch block only traps exceptions thrown during the immediate, synchronous execution tick of the passed function.

If the target function returns a Promise or schedules asynchronous operations (such as setTimeout or fetch), internal rejections or asynchronous exceptions will bypass the synchronous try-catch boundary. For asynchronous operations, native Promise rejection handling (.catch() or async/await with native try-catch) remains necessary.