Promise.try Guide: Complete JavaScript Async Error Handling (2026) | Oleg Maximov
Guide · Updated June 2026

Promise.try Complete Guide:
JavaScript Async Error Handling in 2026

Say goodbye to Promise.resolve().then(fn) and new Promise(resolve => resolve(fn())). Promise.try is here — and it changes how every JavaScript developer handles functions that may throw synchronously or asynchronously. Complete guide with code examples, use cases, and migration strategies.

Oleg Maximov June 1, 2026 12 min read

Introduction

If you've ever written code that needs to handle errors from a function you don't control — a callback, a user-provided handler, or a plugin hook — you've faced the sync/async error handling gap. When a function throws synchronously, your try/catch catches it. When it returns a rejected Promise, your .catch() catches it. But what if you don't know which one it does?

Until now, every JavaScript developer had to write awkward workarounds:

// The old way: wrap in Promise.resolve().then()
Promise.resolve().then(() => riskyFn())
  .then(result => handle(result))
  .catch(err => console.error(err)); // catches both sync throws and async rejects

// But this adds an unnecessary microtask! The callback runs on the next tick.
// And the intent is buried — are we resolving a Promise, or wrapping a function?

Promise.try, a Stage 4 TC39 proposal shipping in Chrome 128+ (late May 2026), Node.js 22+, and Deno 2.8+, solves this elegantly. It wraps any function — sync or async — into a Promise, catching synchronous exceptions immediately (without a microtask delay) while also handling async rejections.

// Promise.try — the modern approach
Promise.try(() => riskyFn())
  .then(result => handle(result))
  .catch(err => console.error(err)); // catches everything, no microtax delay

// Clean. Intentional. No ceremony.

In this guide, I'll cover everything you need to know: what Promise.try is, how it works under the hood, real-world use cases, browser support, migration from old patterns, and a polyfill for production today.

What Is Promise.try?

Promise.try is a static method on the Promise constructor that takes a function as its argument, calls it immediately, and wraps the result (whether it's a plain value, a Promise, or a thrown exception) into a resolved or rejected Promise.

// Basic Promise.try usage
// Sync function returning a value
Promise.try(() => 42)
  .then(n => console.log(n)); // 42

// Sync function that throws
Promise.try(() => { throw new Error("fail"); })
  .catch(err => console.error(err.message)); // "fail"

// Async function (returns a Promise)
Promise.try(async () => {
  const data = await fetch("/api/data");
  return data.json();
})
  .then(json => console.log(json))
  .catch(err => console.error(err));

// Mixed — you don't know if fn is sync or async
function wrap(fn) {
  return Promise.try(fn).then(result => ({ success: true, result }))
                        .catch(err => ({ success: false, error: err }));
}

The key insight: Promise.try calls the function synchronously, then wraps whatever it returns or throws into a Promise. If the function throws, Promise.try catches it immediately and returns a rejected Promise — no microtask, no delay.

The Problem: Why Old Patterns Are Painful

Pattern 1: Promise.resolve().then(fn)

This is the most common workaround, but it has a subtle flaw: the callback always executes on the next microtask, even if the function is synchronous.

// The microtask problem
console.log("1");

Promise.resolve().then(() => {
  console.log("3"); // runs after microtask delay
});

console.log("2");

// Output: 1, 2, 3
// The callback was delayed unnecessarily

With Promise.try, the function executes immediately:

console.log("1");

Promise.try(() => {
  console.log("3"); // runs immediately, before "2"?? No.
  // Actually Promise.try calls fn synchronously but still returns a Promise.
  // The difference: fn() is called NOW, but .then() handlers still queue.
});

console.log("2");

// Output: 1, 2, 3
// fn() was called in the current execution context,
// but what happens inside .then() depends on whether fn returns a Promise or not.

Let me clarify: Promise.try calls the function synchronously in the current execution context, which means synchronous exceptions are caught immediately. But the returned Promise still resolves asynchronously — .then() handlers always run on a microtask. The advantage over Promise.resolve().then(fn) is that fn() itself is not delayed.

Pattern 2: new Promise(resolve => resolve(fn()))

Another common pattern that's verbose and error-prone:

function oldWrap(fn) {
  return new Promise((resolve) => {
    try {
      resolve(fn());
    } catch (e) {
      resolve(e); // should be reject!
    }
  });
}

// Easy to get wrong — the example above accidentally resolves with the error
// instead of rejecting. Promise.try handles this correctly by design.

The new Promise(resolve => resolve(fn())) executor pattern is so frequently buggy that it's become a known anti-pattern. Developers forget to wrap fn() in try/catch, or they resolve instead of reject. Promise.try eliminates this class of bugs entirely.

How Promise.try Works Under the Hood

The spec is remarkably simple. Here's the semantic equivalent:

// Promise.try polyfill (conceptual)
if (!Promise.try) {
  Promise.try = function(fn) {
    return new Promise(function(resolve) {
      resolve(fn());
    });
  };
}

That's it. The Promise constructor executor runs synchronously, so fn() is called immediately. If it throws, the exception is caught by the Promise constructor's implicit try/catch and the Promise rejects. If it returns a value or a Promise, the Promise resolves with it (Promise resolution flattens thenables automatically).

Native implementations (Chrome V8, Node.js) may have additional micro-optimizations, but the semantics are identical.

Real-World Use Cases

1. Middleware and Pipeline Patterns

When building middleware chains where each step may be sync or async, Promise.try provides a clean interface:

// Middleware pipeline with mixed sync/async steps
function createPipeline(...steps) {
  return function(input) {
    return steps.reduce(
      (chain, step) => chain.then(result => Promise.try(() => step(result))),
      Promise.resolve(input)
    );
  };
}

const pipeline = createPipeline(
  (data) => data.trim(),                        // sync
  async (data) => {                              // async
    const validated = await validate(data);
    return validated;
  },
  (data) => data.length > 0 ? data : throw new Error("Empty")  // may throw
);

pipeline(" hello ")
  .then(result => console.log(result))
  .catch(err => console.error(err));

2. Error Boundary for User Callbacks

When your library accepts callbacks from users, you never know if they'll throw synchronously or return a rejected Promise:

// Safe callback invocation
function safeInvoke(callback, ...args) {
  return Promise.try(() => callback(...args))
    .then(result => ({ status: "fulfilled", value: result }))
    .catch(err => ({ status: "rejected", reason: err.message }));
}

// Usage — both work correctly:
safeInvoke((x) => x * 2, 21)
  .then(r => console.log(r));   // { status: "fulfilled", value: 42 }

safeInvoke((x) => { throw new Error("bad!"); }, 10)
  .then(r => console.log(r));   // { status: "rejected", reason: "bad!" }

safeInvoke(async (x) => { throw new Error("async bad!"); }, 10)
  .then(r => console.log(r));   // { status: "rejected", reason: "async bad!" }

3. Retry Wrapper with Consistent Error Handling

// Retry logic with Promise.try
function withRetry(fn, { retries = 3, delay = 1000 } = {}) {
  return Promise.try(fn).catch(function attempt(err, attemptNo = 1) {
    if (attemptNo >= retries) throw err;
    return new Promise(resolve => setTimeout(resolve, delay))
      .then(() => Promise.try(fn))
      .catch(err => attempt(err, attemptNo + 1));
  });
}

// Works for both sync and async functions:
withRetry(() => fetch("/api/data").then(r => r.json()))
  .then(data => console.log(data))
  .catch(err => console.error("All retries failed:", err));

withRetry(() => {
  const config = readConfigFile(); // may throw synchronously
  return process(config);
});

4. Promise.all with Mixed Entries

When mapping over items where some transformations may throw synchronously:

// Safe mapping with Promise.try
async function safeMap(items, fn) {
  const results = await Promise.all(
    items.map(item => Promise.try(() => fn(item)))
  );
  return results;
}

// Even if fn throws for some items, others still resolve:
safeMap([1, 2, 3], (n) => {
  if (n === 2) throw new Error("Two is bad");
  return n * 10;
})
  .then(r => console.log(r))
  .catch(e => console.error(e)); // Only catches if ALL fail

5. Event Handlers and Hooks

Framework event systems often need to handle mixed sync/async handlers:

// Event system with Promise.try
class Emitter {
  constructor() {
    this.handlers = new Map();
  }

  on(event, handler) {
    if (!this.handlers.has(event)) this.handlers.set(event, []);
    this.handlers.get(event).push(handler);
  }

  async emit(event, payload) {
    const handlers = this.handlers.get(event) || [];
    for (const handler of handlers) {
      try {
        await Promise.try(() => handler(payload));
      } catch (err) {
        console.error(`Handler for ${event} failed:`, err);
        // Continue with next handler instead of crashing
      }
    }
  }
}

// Usage:
const emitter = new Emitter();
emitter.on("user:created", (user) => console.log("Sync:", user.name));
emitter.on("user:created", async (user) => {
  await sendWelcomeEmail(user);
});
emitter.emit("user:created", { name: "Alice" }); // both handlers run

Browser Support and Polyfill

Environment Support Notes
Chrome 128+ Supported Shipping late May 2026
Node.js 22+ Supported Via V8 updates
Deno 2.8+ Supported V8 14.9 includes Promise.try
Firefox In Development SpiderMonkey implementation in progress
Safari In Development WebKit signaled positive intent
Safari 26 In Development For a breakdown of all Safari 26 features for web developers, see my Safari 26 complete guide
Older Browsers Polyfill Required Use core-js or the simple polyfill below

Polyfill for Production

// Option 1: Simple polyfill
if (!Promise.try) {
  Promise.try = function(fn) {
    return new Promise(function(resolve) {
      resolve(fn());
    });
  };
}

// Option 2: More robust (handles non-functions, context)
if (!Promise.try) {
  Promise.try = function(fn, ...args) {
    if (typeof fn !== 'function') {
      return Promise.resolve(fn);
    }
    return new Promise(function(resolve) {
      resolve(fn(...args));
    });
  };
}

// Option 3: Use core-js (most complete)
// import 'core-js/actual/promise/try';

Promise.try vs Alternatives

Pattern Sync Error? Async Error? Microtask Delay? Readability
Promise.try(fn) Caught Caught None Excellent
Promise.resolve().then(fn) Caught Caught Always Poor
new Promise(r => r(fn())) Caught Caught None Poor (verbose, easy to get wrong)
async () => fn() Caught Caught Always Moderate
try { await fn() } Caught Caught N/A (in async context) Good (but requires async context)

Pitfalls to Watch For

1. Promise.try Doesn't Accept a Thenable Directly

Promise.try expects a function, not a Promise or thenable. If you already have a Promise, just use it directly:

// Wrong — passing a Promise, not a function:
Promise.try(existingPromise); // TypeError: fn is not a function

// Right — just use the Promise directly:
existingPromise.then(result => ...)

// Right — wrap in a function:
Promise.try(() => existingPromise).then(result => ...)

2. .then() Still Runs on a Microtask

Promise.try calls fn() synchronously, but any .then() handlers still run on the next microtask. You're not making everything synchronous — you're just removing the unnecessary delay on the function call itself.

3. Not All Engines Support It Yet

While Chrome 128+ and Node.js 22+ have native support, Safari and Firefox implementations are still in development. Always include a polyfill for production code targeting a broad audience. Track the latest JavaScript language features and their support status in my ES2026 Complete Guide.

Migration Guide

Migrating from old patterns to Promise.try is straightforward:

// FROM: Promise.resolve().then(() => doWork())
Promise.resolve().then(() => doWork())
  .then(result => handle(result))
  .catch(err => onError(err));

// TO: Promise.try(() => doWork())
Promise.try(() => doWork())
  .then(result => handle(result))
  .catch(err => onError(err));
// FROM: new Promise(resolve => resolve(fetchData()))
new Promise(resolve => resolve(fetchData()))
  .then(process)
  .catch(handleError);

// TO: Promise.try(() => fetchData())
Promise.try(() => fetchData())
  .then(process)
  .catch(handleError);
// FROM: (async () => riskyOp())() — IIFE just to get a Promise
(async () => riskyOp())().then(handle).catch(onError);

// TO: Promise.try(() => riskyOp()).then(handle).catch(onError);

For large codebases, search for Promise.resolve().then( and new Promise(resolve => resolve( patterns. Each can be mechanically replaced with Promise.try(() => ...). The semantic difference (microtask timing) is negligible for the vast majority of cases.

Promise.try and the Larger JavaScript Landscape

Promise.try is part of a broader movement in JavaScript toward safer, more expressive async primitives. Alongside the ES2026 feature set (Temporal API, Pattern Matching, Pipeline Operator), it represents TC39's focus on developer experience and correctness.

If you're building modern Node.js applications, also check out the Node.js 26 Complete Guide which covers the full runtime environment where Promise.try will be the default. For runtime alternatives, see the Deno 2.8 Guide — Deno shipped Promise.try in its V8 14.9 update.

And if you're comparing frontend frameworks, understanding Promise.try is especially valuable when working with React's concurrent features and data fetching patterns. See my React vs Next.js comparison for how modern async patterns fit into React Server Components and Server Actions.

Need reliable async applications? See my development services

FAQ

What is Promise.try in JavaScript?
Promise.try is a new static method on the Promise constructor that wraps a function (sync or async) in a Promise, catching both synchronous throws and asynchronous rejections in one unified handler. Unlike Promise.resolve().then(fn), it executes the function immediately without adding a microtask delay. Example: Promise.try(() => JSON.parse(data)).then(result => handle(result)).catch(err => console.log(err)).
How is Promise.try different from Promise.resolve().then()?
Two key differences: (1) Promise.resolve().then(fn) always adds a microtask delay — the callback runs on the next tick even for synchronous functions. Promise.try calls the function immediately in the current execution context. (2) Promise.try is self-documenting — it clearly communicates "try this function and handle its result as a Promise" whereas Promise.resolve().then(fn) obscures the intent.
Is Promise.try supported in browsers?
Promise.try is a Stage 4 TC39 proposal that ships in Chrome 128+ (May 2026), Node.js 22+, and Deno 2.8+. Safari and Firefox have signaled positive intent. For production targeting older browsers, include the polyfill: Promise.try = Promise.try || (fn => new Promise(resolve => resolve(fn()))). You can safely use it in Node.js 22+ and Deno 2.8+ without a polyfill.
What problems does Promise.try solve?
Three main problems: (1) Synchronous exceptions thrown inside Promise.resolve().then() are caught but only after an unnecessary microtask delay. (2) The Promise.resolve().then(fn) pattern is semantically awkward. (3) Building generic utility functions that accept sync or async callbacks requires awkward boilerplate like (async () => fn())(). Promise.try provides a clean, single-method solution to all three.
Can I polyfill Promise.try for older browsers?
Yes. A simple polyfill: if (!Promise.try) { Promise.try = (fn) => new Promise((resolve) => resolve(fn())); }. More robust polyfills with context binding are available in core-js: import 'core-js/actual/promise/try'. The polyfill works by wrapping the function call in a new Promise constructor, which immediately executes the function and forwards both return values and thrown exceptions.
When should I use Promise.try?
Use Promise.try whenever you need to handle a function's result as a Promise but don't know if the function is sync or async. Common scenarios: middleware/pipeline patterns, wrapper functions (logging, retry, timeout), error boundaries, event handler systems, and generic async utility libraries. If you already know the function is async and returns a Promise, just call it directly — you don't need Promise.try.
Is Promise.try part of ES2025 or ES2026?
Promise.try reached Stage 4 (finished) and was included in the ECMAScript 2025 specification. Engine implementations took until mid-2026 for broad availability — Chrome 128 shipped it in late May 2026. For a complete overview of all new JavaScript features including the Temporal API, Pattern Matching, and more, see my ES2026 Complete Guide.

Start Using Promise.try Today

Promise.try is one of those rare additions to JavaScript that immediately makes you wonder how you lived without it. It's not flashy — it doesn't enable new capabilities you couldn't achieve before. But it makes your code cleaner, safer, and more intentional. Every time you find yourself writing Promise.resolve().then(...) or wrapping a function in async () => fn(), reach for Promise.try instead.

Ready to adopt Promise.try in your projects? Add the polyfill today, start using it in Node.js 22+ or Deno 2.8+ natively, and clean up those awkward Promise wrappers.

If you're planning a web development project and need an experienced full-stack developer who stays current with the latest JavaScript features, reach out to me. I build modern, maintainable applications using the best tools available in 2026.

I'm a full-stack developer with 20+ years of experience building in React, Vue.js, Angular, Node.js, and beyond. Based in Minsk and working worldwide, let's discuss your project.

Contact

Let's discuss your project

Tell me about your project — I'll provide expert advice on architecture, technology choices, and a preliminary estimate. Free of charge.