Why cy.log Prints Nothing

Why cy.log command prints null or undefined even if the variable is set by the Cypress test.

This is a very common question and comes up at least every couple of days on the Cypress chat channel.

Imagine you have an element on the page

1
<div id="username">Mary</div>

You would like to print the text from the element #username to Cypress Command Log. You know that Cypress commands are asynchronous, so you place the value into a variable before calling cy.log command.

1
2
3
4
5
6
7
it('prints the text', () => {
cy.visit('index.html')
// ⛔️ INCORRECT - PRINTS NULL
let username = null
cy.get('#username').then(($el) => (username = $el.text()))
cy.log(username) // always prints null ⚠️
})

Hmm, the Command Log prints NULL.

Cypress Command Log shows NULL instead of text

Let's fix it!

🎁 You can find the source code for this blog post in the repo bahmutov/cypress-log-example.

If you would rather watch the explanation than read it, I have recorded a short video below.

Tip: for more videos about Cypress subscribe to my YouTube channel

The root cause

Cypress commands are asynchronous, but they are also chained first. When Cypress runs through the test the first time, it only internally queues the commands for execution. Every command is stored as an object in Cypress memory together with its arguments. In JavaScript, when you call a function, the primitive arguments are passed by value. Each argument's value at the moment of the call is copied and passed into the function. Let's write down as a comment the command and its argument as stored in memory. For example, cy.visit('index.html') will become an object with the command "VISIT" to run and an argument string index.html. This object is stored in Cypress command chain.

1
2
3
4
5
6
// test code                              chained command with arguments
cy.visit('index.html') // VISIT "index.html"
let username = null
cy.get('#username') // GET "#username"
.then(($el) => (username = $el.text())) // THEN callback function
cy.log(username) // LOG null

At the moment when cy.log(username) is called, the value of the argument is given by the variable username. JavaScript looks up the current value, sees null and then calls cy.log(null). That is JavaScript semantics - it has nothing to do with Cypress' logic.

The solution

We need to delay calling cy.log(username) until the variable username has a value. One solution is to move calling cy.log into its own .then block after the .then(($el) => (username = $el.text())) finishes.

1
2
3
4
5
6
7
8
9
10
cy.visit('index.html')
// ✅ CORRECT - prints "Mary"
let username = null
cy.get('#username')
.then(($el) => (username = $el.text()))
.then(() => {
// by this point the "username" primitive variable
// has been set, and the call is made cy.log("Mary")
cy.log(username)
})

The cy.log prints the text from the page

We do not need a separate .then callback, we can simply log the text immediate as we receive it.

1
2
3
4
5
6
7
8
cy.visit('index.html')
// ✅ CORRECT - prints "Mary"
let username = null
cy.get('#username')
.then(($el) => {
username = $el.text()
cy.log(username)
})

In this case, we do not even need a variable

1
2
3
4
5
6
cy.visit('index.html')
// ✅ CORRECT - prints "Mary"
cy.get('#username')
.then(($el) => {
cy.log($el.text())
})

We do not even need a .then callback. We can invoke the method text and pass the result to the cy.log method.

1
2
3
cy.visit('index.html')
// ✅ CORRECT - prints "Mary"
cy.get('#username').invoke('text').then(cy.log)

Bonus: see the order of command chaining and execution

You can print each Cypress command as it is added to the chain of commands in memory by subscribing to the Cypress command events. You can even print the commands at the start and at the end of their actual execution.

1
2
3
4
5
6
7
8
9
10
11
12
13
it.only('prints null with event trace', () => {
cy.on('command:enqueued', ({ name, args }) => {
console.log(`ENQUEUED ${name}: ${args}`)
})
cy.on('command:start', ({ attributes: { name, args } }) => {
console.log(`START ${name}: ${args}`)
})
cy.visit('index.html')
// ⛔️ INCORRECT - PRINTS NULL
let username = null
cy.get('#username').then(($el) => (username = $el.text()))
cy.log(username) // always prints null ⚠️
})

The DevTools console shows the log command without an argument

Tracing Cypress commands

Let's see the trace for the corrected test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
it('prints text with event trace', () => {
cy.on('command:enqueued', ({ name, args }) => {
console.log(`ENQUEUED ${name}: ${args}`)
})
cy.on('command:start', ({ attributes: { name, args } }) => {
console.log(`START ${name}: ${args}`)
})
cy.visit('index.html')
// ✅ CORRECT - prints "Mary"
cy.get('#username')
.invoke('text')
// avoid printing internals of cy.log in the trace
.then((s) => cy.log(s))
})

Tracing Cypress commands during correct test

Notice in the trace that LOG command was enqueued only after the command invoke('text') has ran. By the time cy.log(s) is added to the queue, the value s exists and is passed to the call.

Bonus 2: cypress-command-chain

To better see the Cypress commands and their parameters in the queue, I have created a plugin called cypress-command-chain. Read the blog post Visualize Cypress Command Queue.

More info