diff --git a/crates/hir/src/diagnostics.rs b/crates/hir/src/diagnostics.rs
index d9ad8db6f76..eaf1a14ec34 100644
--- a/crates/hir/src/diagnostics.rs
+++ b/crates/hir/src/diagnostics.rs
@@ -1,5 +1,5 @@
 //! FIXME: write short doc here
-pub use hir_def::diagnostics::{InactiveCode, UnresolvedModule};
+pub use hir_def::diagnostics::{InactiveCode, UnresolvedModule, UnresolvedProcMacro};
 pub use hir_expand::diagnostics::{
     Diagnostic, DiagnosticCode, DiagnosticSink, DiagnosticSinkBuilder,
 };
diff --git a/crates/hir_def/src/diagnostics.rs b/crates/hir_def/src/diagnostics.rs
index b221b290c56..dd06e3f2005 100644
--- a/crates/hir_def/src/diagnostics.rs
+++ b/crates/hir_def/src/diagnostics.rs
@@ -127,3 +127,65 @@ impl Diagnostic for InactiveCode {
         self
     }
 }
+
+// Diagnostic: unresolved-proc-macro
+//
+// This diagnostic is shown when a procedural macro can not be found. This usually means that
+// procedural macro support is simply disabled (and hence is only a weak hint instead of an error),
+// but can also indicate project setup problems.
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct UnresolvedProcMacro {
+    pub file: HirFileId,
+    pub node: SyntaxNodePtr,
+    pub macro_name: Option<String>,
+}
+
+impl Diagnostic for UnresolvedProcMacro {
+    fn code(&self) -> DiagnosticCode {
+        DiagnosticCode("unresolved-proc-macro")
+    }
+
+    fn message(&self) -> String {
+        match &self.macro_name {
+            Some(name) => format!("proc macro `{}` not expanded", name),
+            None => "proc macro not expanded".to_string(),
+        }
+    }
+
+    fn display_source(&self) -> InFile<SyntaxNodePtr> {
+        InFile::new(self.file, self.node.clone())
+    }
+
+    fn as_any(&self) -> &(dyn Any + Send + 'static) {
+        self
+    }
+}
+
+// Diagnostic: macro-error
+//
+// This diagnostic is shown for macro expansion errors.
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct MacroError {
+    pub file: HirFileId,
+    pub node: SyntaxNodePtr,
+    pub message: String,
+}
+
+impl Diagnostic for MacroError {
+    fn code(&self) -> DiagnosticCode {
+        DiagnosticCode("macro-error")
+    }
+    fn message(&self) -> String {
+        self.message.clone()
+    }
+    fn display_source(&self) -> InFile<SyntaxNodePtr> {
+        InFile::new(self.file, self.node.clone())
+    }
+    fn as_any(&self) -> &(dyn Any + Send + 'static) {
+        self
+    }
+    fn is_experimental(&self) -> bool {
+        // Newly added and not very well-tested, might contain false positives.
+        true
+    }
+}
diff --git a/crates/hir_def/src/nameres.rs b/crates/hir_def/src/nameres.rs
index 202a7dcb6d8..3d65a46bf79 100644
--- a/crates/hir_def/src/nameres.rs
+++ b/crates/hir_def/src/nameres.rs
@@ -286,8 +286,8 @@ mod diagnostics {
     use cfg::{CfgExpr, CfgOptions};
     use hir_expand::diagnostics::DiagnosticSink;
     use hir_expand::hygiene::Hygiene;
-    use hir_expand::InFile;
-    use syntax::{ast, AstPtr};
+    use hir_expand::{InFile, MacroCallKind};
+    use syntax::{ast, AstPtr, SyntaxNodePtr};
 
     use crate::path::ModPath;
     use crate::{db::DefDatabase, diagnostics::*, nameres::LocalModuleId, AstId};
@@ -301,6 +301,10 @@ mod diagnostics {
         UnresolvedImport { ast: AstId<ast::Use>, index: usize },
 
         UnconfiguredCode { ast: AstId<ast::Item>, cfg: CfgExpr, opts: CfgOptions },
+
+        UnresolvedProcMacro { ast: MacroCallKind },
+
+        MacroError { ast: MacroCallKind, message: String },
     }
 
     #[derive(Debug, PartialEq, Eq)]
@@ -348,6 +352,18 @@ mod diagnostics {
             Self { in_module: container, kind: DiagnosticKind::UnconfiguredCode { ast, cfg, opts } }
         }
 
+        pub(super) fn unresolved_proc_macro(container: LocalModuleId, ast: MacroCallKind) -> Self {
+            Self { in_module: container, kind: DiagnosticKind::UnresolvedProcMacro { ast } }
+        }
+
+        pub(super) fn macro_error(
+            container: LocalModuleId,
+            ast: MacroCallKind,
+            message: String,
+        ) -> Self {
+            Self { in_module: container, kind: DiagnosticKind::MacroError { ast, message } }
+        }
+
         pub(super) fn add_to(
             &self,
             db: &dyn DefDatabase,
@@ -407,6 +423,38 @@ mod diagnostics {
                         opts: opts.clone(),
                     });
                 }
+
+                DiagnosticKind::UnresolvedProcMacro { ast } => {
+                    let (file, ast, name) = match ast {
+                        MacroCallKind::FnLike(ast) => {
+                            let node = ast.to_node(db.upcast());
+                            (ast.file_id, SyntaxNodePtr::from(AstPtr::new(&node)), None)
+                        }
+                        MacroCallKind::Attr(ast, name) => {
+                            let node = ast.to_node(db.upcast());
+                            (
+                                ast.file_id,
+                                SyntaxNodePtr::from(AstPtr::new(&node)),
+                                Some(name.to_string()),
+                            )
+                        }
+                    };
+                    sink.push(UnresolvedProcMacro { file, node: ast, macro_name: name });
+                }
+
+                DiagnosticKind::MacroError { ast, message } => {
+                    let (file, ast) = match ast {
+                        MacroCallKind::FnLike(ast) => {
+                            let node = ast.to_node(db.upcast());
+                            (ast.file_id, SyntaxNodePtr::from(AstPtr::new(&node)))
+                        }
+                        MacroCallKind::Attr(ast, _) => {
+                            let node = ast.to_node(db.upcast());
+                            (ast.file_id, SyntaxNodePtr::from(AstPtr::new(&node)))
+                        }
+                    };
+                    sink.push(MacroError { file, node: ast, message: message.clone() });
+                }
             }
         }
     }
diff --git a/crates/hir_def/src/nameres/collector.rs b/crates/hir_def/src/nameres/collector.rs
index 5ed9073e083..19cd713ba04 100644
--- a/crates/hir_def/src/nameres/collector.rs
+++ b/crates/hir_def/src/nameres/collector.rs
@@ -7,7 +7,6 @@ use std::iter;
 
 use base_db::{CrateId, FileId, ProcMacroId};
 use cfg::{CfgExpr, CfgOptions};
-use hir_expand::InFile;
 use hir_expand::{
     ast_id_map::FileAstId,
     builtin_derive::find_builtin_derive,
@@ -16,6 +15,7 @@ use hir_expand::{
     proc_macro::ProcMacroExpander,
     HirFileId, MacroCallId, MacroDefId, MacroDefKind,
 };
+use hir_expand::{InFile, MacroCallLoc};
 use rustc_hash::{FxHashMap, FxHashSet};
 use syntax::ast;
 use test_utils::mark;
@@ -812,7 +812,30 @@ impl DefCollector<'_> {
             log::warn!("macro expansion is too deep");
             return;
         }
-        let file_id: HirFileId = macro_call_id.as_file();
+        let file_id = macro_call_id.as_file();
+
+        // First, fetch the raw expansion result for purposes of error reporting. This goes through
+        // `macro_expand_error` to avoid depending on the full expansion result (to improve
+        // incrementality).
+        let err = self.db.macro_expand_error(macro_call_id);
+        if let Some(err) = err {
+            if let MacroCallId::LazyMacro(id) = macro_call_id {
+                let loc: MacroCallLoc = self.db.lookup_intern_macro(id);
+
+                let diag = match err {
+                    hir_expand::ExpandError::UnresolvedProcMacro => {
+                        // Missing proc macros are non-fatal, so they are handled specially.
+                        DefDiagnostic::unresolved_proc_macro(module_id, loc.kind)
+                    }
+                    _ => DefDiagnostic::macro_error(module_id, loc.kind, err.to_string()),
+                };
+
+                self.def_map.diagnostics.push(diag);
+            }
+            // FIXME: Handle eager macros.
+        }
+
+        // Then, fetch and process the item tree. This will reuse the expansion result from above.
         let item_tree = self.db.item_tree(file_id);
         let mod_dir = self.mod_dirs[&module_id].clone();
         ModCollector {
diff --git a/crates/hir_expand/src/db.rs b/crates/hir_expand/src/db.rs
index 46ebdbc74d5..7ea1c63013b 100644
--- a/crates/hir_expand/src/db.rs
+++ b/crates/hir_expand/src/db.rs
@@ -3,7 +3,7 @@
 use std::sync::Arc;
 
 use base_db::{salsa, SourceDatabase};
-use mbe::{ExpandResult, MacroRules};
+use mbe::{ExpandError, ExpandResult, MacroRules};
 use parser::FragmentKind;
 use syntax::{algo::diff, AstNode, GreenNode, Parse, SyntaxKind::*, SyntaxNode};
 
@@ -81,6 +81,9 @@ pub trait AstDatabase: SourceDatabase {
     ) -> ExpandResult<Option<(Parse<SyntaxNode>, Arc<mbe::TokenMap>)>>;
     fn macro_expand(&self, macro_call: MacroCallId) -> ExpandResult<Option<Arc<tt::Subtree>>>;
 
+    /// Firewall query that returns the error from the `macro_expand` query.
+    fn macro_expand_error(&self, macro_call: MacroCallId) -> Option<ExpandError>;
+
     #[salsa::interned]
     fn intern_eager_expansion(&self, eager: EagerCallLoc) -> EagerMacroId;
 
@@ -171,6 +174,10 @@ fn macro_expand(db: &dyn AstDatabase, id: MacroCallId) -> ExpandResult<Option<Ar
     macro_expand_with_arg(db, id, None)
 }
 
+fn macro_expand_error(db: &dyn AstDatabase, macro_call: MacroCallId) -> Option<ExpandError> {
+    db.macro_expand(macro_call).err
+}
+
 fn expander(db: &dyn AstDatabase, id: MacroCallId) -> Option<Arc<(TokenExpander, mbe::TokenMap)>> {
     let lazy_id = match id {
         MacroCallId::LazyMacro(id) => id,
diff --git a/crates/hir_expand/src/lib.rs b/crates/hir_expand/src/lib.rs
index d5ba691b7dc..6dad2507bf3 100644
--- a/crates/hir_expand/src/lib.rs
+++ b/crates/hir_expand/src/lib.rs
@@ -255,7 +255,7 @@ pub enum MacroDefKind {
 pub struct MacroCallLoc {
     pub(crate) def: MacroDefId,
     pub(crate) krate: CrateId,
-    pub(crate) kind: MacroCallKind,
+    pub kind: MacroCallKind,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs
index 3df73ed4fa3..8b4ceb9a102 100644
--- a/crates/ide/src/diagnostics.rs
+++ b/crates/ide/src/diagnostics.rs
@@ -142,6 +142,13 @@ pub(crate) fn diagnostics(
                     .with_code(Some(d.code())),
             );
         })
+        .on::<hir::diagnostics::UnresolvedProcMacro, _>(|d| {
+            // FIXME: it would be nice to tell the user whether proc macros are currently disabled
+            res.borrow_mut().push(
+                Diagnostic::hint(sema.diagnostics_display_range(d).range, d.message())
+                    .with_code(Some(d.code())),
+            );
+        })
         // Only collect experimental diagnostics when they're enabled.
         .filter(|diag| !(diag.is_experimental() && config.disable_experimental))
         .filter(|diag| !config.disabled.contains(diag.code().as_str()));
diff --git a/docs/user/generated_diagnostic.adoc b/docs/user/generated_diagnostic.adoc
index 34c4f98a3fc..1dfba667038 100644
--- a/docs/user/generated_diagnostic.adoc
+++ b/docs/user/generated_diagnostic.adoc
@@ -17,6 +17,12 @@ This diagnostic is shown for code with inactive `#[cfg]` attributes.
 This diagnostic is triggered if item name doesn't follow https://doc.rust-lang.org/1.0.0/style/style/naming/README.html[Rust naming convention].
 
 
+=== macro-error
+**Source:** https://github.com/rust-analyzer/rust-analyzer/blob/master/crates/hir_def/src/diagnostics.rs#L164[diagnostics.rs]
+
+This diagnostic is shown for macro expansion errors.
+
+
 === mismatched-arg-count
 **Source:** https://github.com/rust-analyzer/rust-analyzer/blob/master/crates/hir_ty/src/diagnostics.rs#L267[diagnostics.rs]
 
@@ -103,3 +109,11 @@ This diagnostic is triggered if rust-analyzer is unable to discover imported mod
 **Source:** https://github.com/rust-analyzer/rust-analyzer/blob/master/crates/hir_def/src/diagnostics.rs#L18[diagnostics.rs]
 
 This diagnostic is triggered if rust-analyzer is unable to discover referred module.
+
+
+=== unresolved-proc-macro
+**Source:** https://github.com/rust-analyzer/rust-analyzer/blob/master/crates/hir_def/src/diagnostics.rs#L131[diagnostics.rs]
+
+This diagnostic is shown when a procedural macro can not be found. This usually means that
+procedural macro support is simply disabled (and hence is only a weak hint instead of an error),
+but can also indicate project setup problems.