← Back to list

Why Your Postman Pre-Request Script Works Locally But Breaks in CI — And How to Fix It

One script. Two runtimes. Zero agreement on async/await. Here's what's actually happening and how to fix it for good.

Martin P Petrov · 2026-03-24 13:01 · 1 claps · 3.5 min read
#postman #newman #api #test-automation #continuous-integration
Open on Medium ↗

Why Your Postman Pre-Request Script Works Locally But Breaks in CI — And How to Fix It

You’ve written a pre-request script in Postman. It works perfectly locally. You push it to CI, Newman runs it, and you get this:

SyntaxError: await is only valid in async functions and the top level bodies of modules

You start questioning your life choices.

This post is for you.

The Setup

A common use case: you need to fetch an OAuth access token before some request. In Postman’s pre-request script, you write something like this:

await new Promise((resolve, reject) => {
    pm.sendRequest({ ... }, (err, res) => {
        pm.environment.set("access_token", res.json().access_token);
        resolve();
    });
});

Clean. Readable. Works perfectly locally. Then Newman blows up on the first await.

The Root Cause: Two Runtimes, Two Different Rules

https://github.com/postmanlabs/newman/issues/3288

Postman and Newman both execute the same pre-request script — but in completely different JavaScript environments.

Postman runs scripts inside a custom JavaScript sandbox. The Postman team extended it to support top-level await— meaning you can use await directly at the top of your script without wrapping it in an async function. Postman also waits for those awaited operations to complete before firing the request.

Newman runs scripts in Node.js using the CommonJS module system. In CommonJS, top-level await is a syntax error — full stop. Node.js throws this error at parse time, before a single line of your script executes. Even if the offending code would never actually be reached at runtime, Newman refuses to proceed.

Why CommonJS Doesn’t Support Top-Level Await

This trips people up because Node.js does support top-level await — but only in ES modules (.mjs files or packages with "type": "module").

CommonJS was designed in 2009, long before async/await existed. It loads modules synchronously — require() blocks until the module is fully loaded. There's no mechanism for a CommonJS module to pause and say "wait, I'm not ready yet." Bolting top-level await onto it would break that synchronous contract entirely, so it was never added.

ES modules, by contrast, were designed from the ground up with async loading in mind. Top-level await fits naturally there.

Newman treats Postman pre-request scripts as CommonJS. Postman built their own sandbox and cherry-picked top-level await support regardless. Two runtimes, two different rules.

The Trap: Why You Can’t Just Wrap Everything in an IIFE

The obvious fix is to wrap everything in an async IIFE (Immediately Invoked Function Expression) to avoid top-level await:

(async () => {
    await new Promise((resolve, reject) => {
        pm.sendRequest({ ... }, (err, res) => {
            pm.environment.set("access_token", res.json().access_token);
            resolve();
        });
    });
})();

Valid syntax everywhere. Newman is happy. But now your local Postman runs are broken — variables never get set, requests fire with missing auth headers, tests fail.

Why? Because Postman does not wait for an IIFE to complete before executing the request. The IIFE fires and Postman immediately continues — your pm.sendRequest callback hasn't run yet, your environment variables haven't been set, and the request goes out unauthenticated.

So you’re stuck:

There is no single script that satisfies both runtimes out of the box.

The Ugly Solution (not recommended but if Newman is a must): Transform the pre-request script in CI

Keep your script written for Postman (with top-level await), create a small Node.js script that downloads your Postman collection, finds the pre-request script in it and automatically wraps it in an IIFE as part of your CI pipeline before handing it to Newman. Each runtime gets the syntax it needs from the same source script.

The Real Solution (in my humble opinion): Use Postman CLI over Newman in CI

Postman CLI uses the same JavaScript sandbox as Postman desktop. Top-level await works, your pre-request scripts behave identically to local runs, and there’s no need for workarounds like IIFE wrapping and script transformation steps.

If your collection has any async logic in pre-request scripts — JWT signing, token fetching, crypto operations — Postman CLI is the correct tool for CI.

TL;DR;

  • Postman supports top-level await in pre-request scripts. Newman doesn’t — it throws a syntax error at parse time.
  • Wrapping in an async IIFE fixes the syntax error but breaks Postman, which doesn’t wait for the IIFE to finish.
  • If you must use Newman: keep your script written for Postman and wrap it in an IIFE automatically in CI before Newman runs it.
  • Better yet: switch to Postman CLI — it uses the same sandbox as Postman desktop and the problem goes away entirely.

Postman’s own comparison between Newman and Postman CLI focuses on features like reporting, command syntax, and CI/CD integration. What it doesn’t mention is the JavaScript sandbox difference – which is exactly the kind of thing you only discover after hours of debugging a SyntaxError that makes no sense on the surface.

Hopefully this saves you the hours it cost me.


메타데이터
post_id
86907bb50ccf
slug
why-your-postman-pre-request-script-works-locally-but-breaks-in-ci-and-how-to-fix-it-86907bb50ccf
url
https://medium.com/@martin.p.petrov/why-your-postman-pre-request-script-works-locally-but-breaks-in-ci-and-how-to-fix-it-86907bb50ccf
canonical_url
https://medium.com/@martin.p.petrov/why-your-postman-pre-request-script-works-locally-but-breaks-in-ci-and-how-to-fix-it-86907bb50ccf
author_url
https://medium.com/@martin.p.petrov
status
ok
fetched_at
2026-07-13 06:23:13