Tiago Sampaio

main ai engineering career

engineering

12 min read

How to Fix: “Area code is not set” in Magento 2

Getting 'Area code is not set' in Magento 2 or Adobe Commerce? See why it happens in CLI commands, patches and scripts, and 4 ways to fix it.

magento-2-area-code-is-not-set-fix-1.png — 1920 × 1080

Cover: How to Fix "Area code is not set" in Magento 2. Panel showing who sets the area: web requests via Http::launch(), cron via crontab, and the CLI where nobody sets it. Based on Magento 2.4.7-p10, 2.4.8-p5 and 2.4.9.

You wrote a small CLI command, a data patch or a quick script to fix some products. You run it, and Magento stops you with one short line: Area code is not set. No stack trace that makes sense at first sight, no hint about what an “area” is or who was supposed to set it.

This is one of the most common errors in Magento 2 and Adobe Commerce, and it almost always shows up outside the browser: in bin/magento, in setup patches, in standalone PHP scripts. The good news is that the cause is always the same, and once you understand it, the fix takes a few lines.

In this post of the Magento Fix Log, I’ll show where the error comes from in the core code, why it happens, and four ways to fix it depending on where your code runs. Everything here was checked against the code of Magento 2.4.7-p10, 2.4.8-p5 and 2.4.9, with notes where the versions behave differently.

The error and the quick fix

This is what you usually see in the terminal:

Shell
In State.php line 149:

  Area code is not set

The exception is thrown on line 149 of State.php in 2.4.7-p10, 2.4.8-p5 and 2.4.9. Or, in a log file:

Shell
Magento\Framework\Exception\LocalizedException: Area code is not set

If you are in a hurry, here is the short version. Your code runs in a context where nobody told Magento which area it is in, and something in the call stack asked for it. Set the area before that code runs:

PHP
use Magento\Framework\App\Area;
use Magento\Framework\App\State;

// inside execute() of your CLI command, before the real work
$this->state->setAreaCode(Area::AREA_ADMINHTML);

That solves most cases. The rest of this post explains why, when to use emulateAreaCode() instead, and how to avoid the trap that breaks every bin/magento command at once.

What an “area” is in Magento 2

Magento splits the application into areas. Each area can have its own dependency injection config (etc/adminhtml/di.xml, etc/frontend/di.xml), its own events, layouts, themes and translations. The list lives in Magento\Framework\App\Area:

ConstantValueUsed by
AREA_GLOBALglobalconfig shared by every area
AREA_FRONTENDfrontendthe storefront
AREA_ADMINHTMLadminhtmlthe admin panel
AREA_CRONTABcrontabcron jobs
AREA_WEBAPI_RESTwebapi_restREST API
AREA_WEBAPI_SOAPwebapi_soapSOAP API
AREA_GRAPHQLgraphqlGraphQL

There is also AREA_DOC (doc) and a deprecated AREA_ADMIN (admin). You should not need either of them.

The important part is who sets the area. In a web request, Magento\Framework\App\Http::launch() resolves the area from the URL front name, calls setAreaCode() and loads that area’s DI config. For cron, Magento\Framework\App\Cron::launch() sets the area to crontab.

The CLI is different. I searched Magento\Framework\Console\Cli, the class behind bin/magento, and it never sets an area code. When your command starts, there is no area. Setup patches and standalone scripts are in the same situation.

Where the error comes from in the core

The message is thrown in exactly one place, Magento\Framework\App\State::getAreaCode():

PHP
public function getAreaCode()
{
    if (!isset($this->_areaCode)) {
        throw new \Magento\Framework\Exception\LocalizedException(
            new \Magento\Framework\Phrase('Area code is not set')
        );
    }
    return $this->_areaCode;
}

So the error doesn’t mean your code is wrong. It means some code in the call stack asked “which area am I in?” and nobody had answered yet. Your command can be a few lines long and still hit it, because the question is often asked deep inside the core.

A few real examples that ask for the area. All of them are present in 2.4.7-p10, 2.4.8-p5 and 2.4.9:

  • Magento\Catalog\Model\Product::getIdentities() checks if the area is frontend. When you save a product and the full page cache is enabled (built-in or Varnish), the clean_cache_by_tags observers collect the product’s identities, and that check runs. This is the classic “I saved a product from a CLI command and it blew up” case.
  • Magento\Theme\Model\View\Design and Magento\Theme\Model\Theme\Resolver read the area to decide which theme applies.
  • Magento\Framework\Url reads it to resolve route front names, so generating URLs from the CLI can trigger it.
  • Magento\Framework\View\Element\Template::getArea() falls back to the current area when the block has none.
  • Customer plugins and observers such as CustomerNotification and AfterAddressSaveObserver check if the request is from the frontend.

That is why the same command works on one store and fails on another. Enable the full page cache, install a third party module with an observer that checks the area, and code that was fine yesterday starts failing.

Fix 1: Set the area in your CLI command

For a custom command, set the area at the beginning of execute(). This is the same pattern the core uses in Magento\MediaStorage\Console\Command\ImagesResizeCommand and Magento\Catalog\Console\Command\ProductAttributesCleanUp, which both call setAreaCode(Area::AREA_GLOBAL).

PHP
<?php
declare(strict_types=1);

namespace Vendor\Module\Console\Command;

use Magento\Framework\App\Area;
use Magento\Framework\App\State;
use Magento\Framework\Exception\LocalizedException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class FixProductsCommand extends Command
{
    public function __construct(
        private readonly State $state,
        private readonly \Vendor\Module\Service\ProductFixer $productFixer
    ) {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this->setName('vendor:products:fix');
        $this->setDescription('Fix product data');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        try {
            $this->state->setAreaCode(Area::AREA_ADMINHTML);
        } catch (LocalizedException $e) {
            // The area was already set by something else. Keep it.
        }

        $this->productFixer->run();

        return Command::SUCCESS;
    }
}

Two details matter here.

First, set it in execute(), never in the constructor. I’ll explain why in Fix 3.

Second, the try/catch. setAreaCode() can only be called once per process. If the area is already set, Magento throws the twin error: Area code is already set. Catching it keeps your command safe when another command or a test has set the area first.

Pick the area based on what your code does. If it behaves like an admin action (saving products, customers, orders), use adminhtml. If you only need global config and plain models, global is enough, like the core image resize command.

Fix 2: Use emulateAreaCode() for a block of code

Sometimes you don’t own the whole process. A data patch runs inside setup:upgrade together with hundreds of other patches. A service can be called from the admin, from cron and from a command. Setting the area for the whole process there is a bad idea.

For these cases, State::emulateAreaCode() runs a callback inside an area and then restores the previous one:

PHP
use Magento\Framework\App\Area;

$this->state->emulateAreaCode(
    Area::AREA_ADMINHTML,
    function () use ($product) {
        $this->productRepository->save($product);
    }
);

The core does exactly this in setup patches. Magento\CatalogSearch\Setup\Patch\Data\SetInitialSearchWeightForAttributes wraps an indexer state change in emulateAreaCode(Area::AREA_CRONTAB, ...), and several Adobe Commerce staging patches do the same with the admin area.

There are two things most tutorials don’t mention.

It only switches the area code. Look at the method and you’ll see it changes _areaCode and nothing else. It doesn’t change the config scope and doesn’t load the area’s DI config. The core knows this: Magento\Config\Console\Command\EmulatedAdminhtmlAreaProcessor wraps emulateAreaCode() and also calls ScopeInterface::setCurrentScope('adminhtml'), then restores the scope at the end (simplified here):

PHP
public function process(callable $callback, array $params = [])
{
    $currentScope = $this->scope->getCurrentScope();
    try {
        return $this->state->emulateAreaCode(Area::AREA_ADMINHTML, function () use ($callback, $params) {
            $this->scope->setCurrentScope(Area::AREA_ADMINHTML);
            return call_user_func_array($callback, array_values($params));
        });
    } finally {
        $this->scope->setCurrentScope($currentScope);
    }
}

If your code depends on plugins or preferences declared in etc/adminhtml/di.xml, emulating the area code is not enough. See Fix 4.

The previous area is restored in a finally block. In 2.4.7-p10, 2.4.8-p5 and 2.4.9, the area goes back to what it was even when your callback throws a PHP Error, like a TypeError. If you still maintain a 2.4.6 store, be careful: there the restore only ran after a normal return or an \Exception, so an Error could leave the emulated area active for the rest of the process.

Fix 3: Keep the constructor clean with proxies

This one surprises people. You add a new command to your module, and suddenly bin/magento setup:upgrade, cache:flush and every other command fail with Area code is not set.

The reason is in Magento\Framework\Console\Cli::getApplicationCommands(). To build the command list, it creates CommandListInterface, and every command registered there is instantiated with its dependencies. That happens on every bin/magento run, whatever command you call. If any constructor in that dependency tree throws, the whole list fails, not only your command.

What you see next depends on the version:

  • 2.4.7-p10 and 2.4.8-p5: Cli stores the exception and Cli::doRun() throws it again. bin/magento ends with Area code is not set, whatever command you ran, including the ones you need to fix the problem.
  • 2.4.9 in developer mode: same result. The exception is thrown right away.
  • 2.4.9 in any other mode: Cli now catches the failure, logs it and keeps going. The error itself has the exception attached, so Magento’s logger writes it to var/log/exception.log, while the terminal message points you to var/log/system.log. Check both. You get a “MAGENTO CLI ERROR (Production Mode)” box saying that core commands like cache:flush and deploy:mode:set are unavailable, with suggestions to clear generated/code, var/cache and var/page_cache and to disable the broken module.

So 2.4.9 is friendlier, but the problem is the same: one bad constructor takes down every command registered through the core command list.

Two rules avoid it:

  1. Never call setAreaCode(), getAreaCode() or area-dependent code inside a command’s constructor.
  2. Inject heavy or area-dependent dependencies as proxies, so they are only created when execute() actually uses them.

The core does this in vendor/magento/module-media-storage/etc/di.xml for the image resize command:

XML
<type name="Magento\MediaStorage\Console\Command\ImagesResizeCommand">
    <arguments>
        <argument name="appState" xsi:type="object">Magento\Framework\App\State\Proxy</argument>
        <argument name="imageResize" xsi:type="object">Magento\MediaStorage\Service\ImageResize\Proxy</argument>
        <argument name="imageResizeScheduler" xsi:type="object">Magento\MediaStorage\Service\ImageResizeScheduler\Proxy</argument>
    </arguments>
</type>

Do the same for your command in your module’s etc/di.xml:

XML
<type name="Vendor\Module\Console\Command\FixProductsCommand">
    <arguments>
        <argument name="productFixer" xsi:type="object">Vendor\Module\Service\ProductFixer\Proxy</argument>
    </arguments>
</type>

Proxies are generated classes. In developer mode Magento generates them on demand. In production mode, run bin/magento setup:di:compile after adding them.

If your whole bin/magento is already broken, disable the module directly in app/etc/config.php (set it to 0), fix the constructor, then enable it again.

Fix 4: Standalone scripts and full area loading

Many developers still keep small PHP scripts at the Magento root for one-off jobs. Those scripts start with no area, just like the CLI:

PHP
<?php
use Magento\Framework\App\Area;
use Magento\Framework\App\Bootstrap;
use Magento\Framework\App\State;

require __DIR__ . '/app/bootstrap.php';

$bootstrap = Bootstrap::create(BP, $_SERVER);
$objectManager = $bootstrap->getObjectManager();

$state = $objectManager->get(State::class);
$state->setAreaCode(Area::AREA_ADMINHTML);

// your code here

setAreaCode() fixes the error, but it doesn’t load the area’s DI config. In a web request, Http::launch() does both: it sets the area and calls $this->_objectManager->configure($this->_configLoader->load($areaCode)). If you need plugins and preferences from etc/adminhtml/di.xml to apply, do the same thing. The core indexer commands show the pattern in Magento\Indexer\Console\Command\AbstractIndexerCommand::getObjectManager():

PHP
$appState = $objectManager->get(\Magento\Framework\App\State::class);
$appState->setAreaCode(\Magento\Backend\App\Area\FrontNameResolver::AREA_CODE);

$configLoader = $objectManager->get(\Magento\Framework\ObjectManager\ConfigLoaderInterface::class);
$objectManager->configure($configLoader->load(\Magento\Backend\App\Area\FrontNameResolver::AREA_CODE));

FrontNameResolver::AREA_CODE is the string adminhtml, the same value as Area::AREA_ADMINHTML.

My honest advice: if the script will run more than once, turn it into a proper CLI command. You get DI, proxies, a clean area setup, and a command your team can find with bin/magento list.

The twin error: “Area code is already set”

The same class throws the opposite error in setAreaCode():

PHP
if (isset($this->_areaCode)) {
    throw new \Magento\Framework\Exception\LocalizedException(
        new \Magento\Framework\Phrase('Area code is already set')
    );
}

You see this one when code calls setAreaCode() in a process that already has an area. Typical cases:

  • calling setAreaCode() inside a cron job, where Cron::launch() already set crontab
  • calling it in a controller, plugin or observer during a web request
  • two commands that each set the area running in the same process, for example in integration tests

The fix is the opposite of Fix 1. Don’t set the area in shared code. Use emulateAreaCode() there, and keep setAreaCode() for the entry point you own, wrapped in a try/catch like the example above.

There is also a third message in the same file: Area code "%1" does not exist. It means you passed a string that isn’t a registered area, usually a typo like admin instead of adminhtml. Use the Area constants and it won’t happen.

Checklist before you ship

  • Is the area set in the entry point you own (execute(), the top of a script), and nowhere else?
  • Does any command constructor touch the area or area-dependent services? If yes, move the logic to execute() and use proxies.
  • In shared services, patches and observers, are you using emulateAreaCode() instead of setAreaCode()?
  • Does your code need etc/adminhtml/di.xml to apply? If yes, load the area config, not just the area code.
  • Still on 2.4.6 somewhere? If your emulated callback can throw an Error, handle it, or the area may not be restored.
  • Did you test with the full page cache enabled, the way production runs?

The rule I follow on client projects is simple: the entry point decides the area, and everything else either works in any area or emulates the one it needs.

Which one got you: a product save from the CLI, or a constructor that broke every bin/magento command?

Sources

All code references were checked in Magento Open Source / Adobe Commerce 2.4.7-p10, 2.4.8-p5 and 2.4.9 (2.4.7-p10 from the open source magento/magento2 repository on GitHub, the other two from full Adobe Commerce installs). The 2.4.6 note comes from 2.4.6-p3 and 2.4.6-p5:

  • vendor/magento/framework/App/State.php: getAreaCode(), setAreaCode(), emulateAreaCode()
  • vendor/magento/framework/App/Area.php: area constants
  • vendor/magento/framework/App/Http.php and vendor/magento/framework/App/Cron.php: where web requests and cron set the area
  • vendor/magento/framework/Console/Cli.php: how bin/magento builds its command list
  • vendor/magento/module-catalog/Model/Product.php: getIdentities()
  • vendor/magento/module-page-cache/Observer/FlushCacheByTags.php and vendor/magento/module-cache-invalidate/Observer/InvalidateVarnishObserver.php
  • vendor/magento/module-media-storage/Console/Command/ImagesResizeCommand.php and vendor/magento/module-media-storage/etc/di.xml
  • vendor/magento/module-config/Console/Command/EmulatedAdminhtmlAreaProcessor.php
  • vendor/magento/module-catalog-search/Setup/Patch/Data/SetInitialSearchWeightForAttributes.php
  • vendor/magento/module-indexer/Console/Command/AbstractIndexerCommand.php

Written by

TS

Tiago Sampaio

Adobe Commerce & Magento 2 Expert

Senior developer with 15+ years building software. Writes practical fixes for Magento 2 and Adobe Commerce, and honest notes on growing as a developer.

$ git remote add inbox

Get new commits by email

New posts from both branches, engineering and career. No spam, unsubscribe any time.

$ git notes show

Leave a Reply