A module hook that returns an empty string looks exactly like a module hook that was never called. PrestaShop gives you nothing to tell them apart: no error, no log entry, no stack trace, no fallback text. The page renders fine. Your block is just absent.
We spent three releases of one module chasing this, and the cause turned out to be three different mechanisms stacked on top of each other. Each one alone is enough to make output vanish silently. This is what they are, in the order we peeled them off.
The setup
The module registers displayHeader and renders a small template: a <script> block that carries a public site key into the page, and a <style> block that hides a third-party badge. Roughly:
public function hookDisplayHeader($params)
{
$this->context->smarty->assign([
'recaptcha_pubkey' => $this->getActivePublicKey(),
'recaptcha_hide_badge' => $hideBadge,
]);
return $this->display(__FILE__, 'views/templates/front/header_script.tpl');
}
Deployed, cache cleared, hook registered, Design > Positions shows the module attached. Page source: nothing. Not the script, not the style, not even a stray whitespace.
Mechanism 1: core swallows the exception
Hook::callHookOn() wraps every module hook call in a try/catch. When debug mode is off, it catches whatever the hook throws and returns an empty string. No error, no log, no trace.
That is a defensible design decision — one broken module should not take down a storefront — but as a debugging experience it is brutal. Every possible failure inside your hook, from a typo to a missing file to a template that will not compile, arrives at your screen as the exact same symptom: nothing.
The first thing to do, before theorising about causes, is to stop letting core swallow it:
try {
return $this->display(__FILE__, 'views/templates/front/header_script.tpl');
} catch (Throwable $e) {
$message = 'mymodule header_script.tpl render failed: ' . $e->getMessage()
. ' in ' . $e->getFile() . ':' . $e->getLine();
PrestaShopLogger::addLog($message, 3, null, 'Mymodule');
return '<!-- ' . str_replace('--', '- -', $message) . ' -->';
}
Two outputs on purpose. The log entry survives after the page is gone and shows up under Advanced Parameters > Logs. The HTML comment is right there in view-source while you are looking at the page, and it survives filters that strip <script> and <style> tags specifically — which matters, because the thing you are debugging may itself be a script tag.
Throwable, not Exception. A TypeError is an Error, and catch (Exception $e) sails straight past it. That distinction has cost us time in more than one module.
Turning debug mode on would also have surfaced it, and if you can reproduce the problem locally you should. We could not: this only appeared on a hosted environment we do not control.
Mechanism 2: one CSS brace kills the whole template
With the try/catch in place, the log finally said something. The template would not compile.
The offending line was plain CSS:
.grecaptcha-badge{visibility:hidden!important;}
Smarty's default delimiters are { and }. It reads {visibility:hidden!important;} as a template tag, tries to parse visibility:hidden as a Smarty expression, chokes on the colon, and fails to compile the file.
The part that turns a small bug into a mystery: Smarty compiles a template as a single unit. A syntax error anywhere fails the whole file. The <script> block sitting above that CSS was perfectly valid and had nothing to do with the problem, and it never rendered either — for three releases we were debugging the script, which was fine, because the thing breaking it was thirty lines below.
The fix is Smarty's own mechanism for raw text that contains braces:
{literal}
.grecaptcha-badge { visibility: hidden !important; }
{/literal}
If you would rather not think about it every time: any { immediately followed by a letter is a Smarty tag. { visibility with a space is not. But {literal} states the intent, and intent is what you want in a file someone else will edit.
This applies to inline JavaScript too — object literals, arrow function bodies, anything with a brace next to a word.
Mechanism 3: the platform rejected nofilter
Before we found the brace, we had already removed a nofilter from the same template. That one is worth recording because the reasoning that led us to it was sound and the fix was correct, even though it was not the bug we were hunting.
The template embedded a value into inline JavaScript the way PrestaShop's own theme templates do:
var key = {$recaptcha_pubkey|json_encode nofilter};
PrestaShop Cloud's integration checklist explicitly requires {$variable|escape:'javascript':'UTF-8'} for values used in inline JS, and forbids nofilter. A platform-side Smarty security policy rejecting nofilter at compile time would produce exactly the signature we were seeing: a template that will not compile, on that host only, silently.
We could not prove that was happening — the brace bug was masking everything — but the guidance holds on its own terms. Use the documented escaper.
The general lesson is narrower than "avoid nofilter": a template that compiles on your machine can fail to compile on a managed host with a stricter Smarty policy, and it fails the same silent way. If your module ships to hosts you do not control, the try/catch from mechanism 1 is not a debugging aid you remove afterwards. Leave it in.
What we changed permanently
-
Every hook that renders a template is wrapped. Log plus HTML comment, catching
Throwable. - No hand-built HTML or JS inside PHP. PrestaShop's integration checklist asks for this anyway, and it means every failure of this class now happens in a template, where the try/catch can see it.
-
Assets carry a version. Separate problem, same family of "I fixed it but nothing changed":
registerJavascript($id, $path, ['version' => $this->version]). Without it there is no cache-buster, and returning visitors keep running the previous file. There is a further trap with PrestaShop's combined asset cache that theversionparameter does not solve — that is its own post.
The shape of the lesson
Silent failure is not one bug, it is a category. When a system is designed to keep serving pages no matter what a plugin does, it will also keep serving pages when your plugin is broken, and it owes you nothing in the way of an explanation.
The response is not to guess better. It is to make the silence impossible: catch what the platform would have swallowed, write it somewhere that outlives the request, and put a marker in the page you are already looking at. We got three releases of guessing before we did that, and roughly one afternoon of actual debugging afterwards.
We build and maintain around sixty PrestaShop modules at MEG Venture, which is a productive way to accumulate stories like this one. If you have hit a variant of this — especially the Smarty brace, which we suspect is more common than its search results suggest — I would like to hear about it.
This article was originally published by DEV Community and written by MEG Venture & Consulting Ltd..
Read original article on DEV Community