Output<T> wrapper to solve this temporal paradox.1. The Temporal Paradox#
Consider this standard TypeScript code:
const bucket = new aws.s3.Bucket("my-bucket");
// Let's print the bucket's Amazon Resource Name (ARN)
console.log(bucket.arn);If you run pulumi up, what do you think console.log will print?
You might expect: arn:aws:s3:::my-bucket-1a2b3c4.
Instead, it prints something like:
OutputImpl { __pulumiOutput: true, ... }
Why?
Because the Node.js language host executes your index.ts file in milliseconds. It sends the intent to create a bucket to the Pulumi Engine. But the physical bucket on AWS takes 15 seconds to create.
Therefore, at the exact moment console.log runs, the ARN simply does not exist in the universe. Pulumi handles this by wrapping all asynchronous data in a special type called Output<T>.
- A standard string is type
string. - A string that will eventually exist in the future is type
pulumi.Output<string>.
2. The Golden Rule of Outputs#
You cannot pass a pulumi.Output<T> into a standard JavaScript function (like .split() or console.log()) that expects a raw T.
If you try to manipulate the string before it resolves, TypeScript will throw an error:
// ERROR: Property 'toUpperCase' does not exist on type 'Output<string>'.
const upperArn = bucket.arn.toUpperCase();However, Pulumi Resources natively accept Outputs as Inputs.
When we built our EC2 instance in Episode 3, we did this:
const subnet = new aws.ec2.Subnet("sub", { ... });
const ec2 = new aws.ec2.Instance("web", {
// We are passing an Output<string> into a field that expects a string!
subnetId: subnet.id
});The subnetId property on the EC2 class is defined as an Input<string>.
An Input<T> is a union type that means: “I will accept either a raw string right now, OR a pulumi.Output<string> that will resolve later.”
Pulumi’s engine handles unwrapping the Output internally and delaying the creation of the EC2 instance until the Subnet ID resolves.
3. Manipulating Outputs with .apply()#
What if you must manipulate the string? For example, you want to take an EC2 instance’s IP address, and format it into a full connection string: mysql://admin:password@<IP_ADDRESS>:3306.
You cannot use standard string concatenation:
// WRONG: This will result in "mysql://admin:password@OutputImpl/..."
const connectionString = "mysql://admin:password@" + db.address + ":3306";To manipulate an Output, you must use the .apply() method. This is conceptually identical to .then() in standard JavaScript Promises.
.apply() accepts a callback function. Pulumi will pause execution of that specific block of code until the AWS API returns the actual raw string, unwrap it, pass it into your callback, and then re-wrap the result in a new Output.
// CORRECT: Using .apply()
const connectionString = db.address.apply(ip => {
// Inside this callback, 'ip' is a raw, physical string!
return `mysql://admin:password@${ip}:3306`;
});
// connectionString is now a pulumi.Output<string>
4. The Interpolation Shortcut#
Using .apply() for simple string concatenation is verbose. Because concatenation is so common (e.g., building URLs or IAM ARNs), Pulumi provides a helper function: pulumi.interpolate.
pulumi.interpolate acts exactly like a JavaScript template literal, but it natively understands and unwraps Output types safely!
// THE BEST WAY:
const connectionString = pulumi.interpolate`mysql://admin:password@${db.address}:3306`;Under the hood, pulumi.interpolate is just calling .apply() on all the variables injected into it.
5. Outputs vs Promises#
If you are a seasoned JavaScript developer, you might be wondering: Why didn’t Pulumi just use standard native Promises?
Pulumi does use Promises for some operations (specifically, querying data that already exists via aws.ec2.getVpc()).
However, Output<T> is a wrapper around a Promise that tracks extra metadata necessary for the Pulumi Engine. Specifically:
- Dependency Tracking: An Output knows which resource created it. When you pass
subnet.idto the EC2 instance, the Output tells the engine to draw a dependency edge between the two resources. A raw Promise cannot do this. - Secret Propagation: If an Output contains a password (like from
config.requireSecret()), the Output is internally flagged asisSecret: true. If you use.apply()to concatenate that password into a connection string, the resulting new Output is automatically flagged as a Secret, preventing accidental leakage.
Troubleshooting & Common Errors#
Calling toString on an Output is not supported- Root Cause: You tried to use standard JavaScript string interpolation (
`The ip is ${web.publicIp}`) instead ofpulumi.interpolate. JavaScript automatically called.toString()on the Output object. - Solution: Always use
pulumi.interpolateor.apply().
- Root Cause: You tried to use standard JavaScript string interpolation (
Output values cannot be used in loops or conditionals- Root Cause: You tried to do this:
if (bucket.arn === "something"). You cannot branch your infrastructure logic based on a value that hasn’t been created yet. - Solution: You must restructure your logic. If the conditional logic depends on cloud state, you must move the conditional inside an
.apply()block, or rethink your architecture.
- Root Cause: You tried to do this:
Conclusion & Next Steps#
Congratulations, you have mastered the most difficult concept in Pulumi. You now understand how the Language Host interacts asynchronously with the Pulumi Engine, and how to safely manipulate data that hasn’t been created yet using Output.apply() and pulumi.interpolate.
You have completed the Pulumi Fundamentals tier!
In Episode 6: Loops and Conditional Infrastructure, we will transition to Intermediate Platform Engineering. We will leverage standard TypeScript arrays, .map(), and .filter() to programmatically generate dozens of resources from simple JSON configurations.

